diff --git a/app/Console/Commands/SendRecurringInvoices.php b/app/Console/Commands/SendRecurringInvoices.php index abf493d1ca54..574d324d3842 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() @@ -48,11 +51,49 @@ 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 || $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 + 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 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); + } + + $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); + } + } + $this->info('Done'); } diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 6882a44be1e6..4fa6e1b90126 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) { @@ -1133,6 +1142,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..629932c567e8 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) ]; } @@ -301,7 +295,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')); } @@ -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/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 ba025e283f1b..8d66e18293d1 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); @@ -110,6 +111,8 @@ class ClientPortalController extends BaseController if($braintreeGateway->getPayPalEnabled()) { $data['braintreeClientToken'] = $this->paymentService->getBraintreeClientToken($account); } + } elseif ($wepayGateway = $account->getGatewayConfig(GATEWAY_WEPAY)){ + $data['enableWePayACH'] = $wepayGateway->getAchEnabled(); } $showApprove = $invoice->quote_invoice_id ? false : true; @@ -161,6 +164,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) { @@ -176,8 +191,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('; } @@ -211,43 +226,39 @@ class ClientPortalController extends BaseController 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; + 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; } - foreach($types as $type) { - $typeLink = strtolower(str_replace('PAYMENT_TYPE_', '', $type)); - $url = URL::to("/payment/{$invitation->invitation_key}/{$typeLink}"); + $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 - ]; - - if($gateway->getPayPalEnabled()) { - $paymentTypes[] = [ - 'label' => trans('texts.paypal'), - 'url' => $url = URL::to("/payment/{$invitation->invitation_key}/braintree_paypal"), - ]; - } + // 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 + ]; } } @@ -277,13 +288,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) { @@ -292,6 +302,7 @@ class ClientPortalController extends BaseController $data = [ 'color' => $color, + 'contact' => $contact, 'account' => $account, 'client' => $client, 'clientFontUrl' => $account->getFontsUrl(), @@ -310,12 +321,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 +353,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 +368,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 +380,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 +395,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 +407,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 +451,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; }) @@ -458,9 +471,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; } @@ -502,11 +522,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 +548,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 +583,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 +598,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 +627,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 +704,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 +737,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,16 +758,17 @@ 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); $data = array( 'account' => $account, + 'contact' => $contact, 'color' => $account->primary_color ? $account->primary_color : '#0b4d78', 'client' => $client, 'clientViewCSS' => $account->clientViewCSS(), @@ -780,11 +793,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 +811,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,21 +829,20 @@ 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) { - $sourceReference = $this->paymentService->createToken($this->paymentService->createGateway($accountGateway), array('token'=>$token), $accountGateway, $client, $invitation->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); @@ -846,7 +858,7 @@ class ClientPortalController extends BaseController $data = [ 'showBreadcrumbs' => false, 'client' => $client, - 'contact' => $invitation->contact, + 'contact' => $contact, 'gateway' => $gateway, 'accountGateway' => $accountGateway, 'acceptedCreditCardTypes' => $acceptedCreditCardTypes, @@ -859,8 +871,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'); } @@ -869,7 +885,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; } @@ -878,20 +894,21 @@ 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); $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')); } @@ -903,21 +920,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, $invitation->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')); @@ -929,12 +951,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 +986,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/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php index da9b90467350..dcef1b61464e 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/Http/Controllers/PaymentController.php b/app/Http/Controllers/PaymentController.php index c0dd4a9f8550..fcd6411cddc5 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,7 +153,26 @@ class PaymentController extends BaseController Session::put($invitation->id.'payment_ref', $invoice->id.'_'.uniqid()); - if ($paymentType != PAYMENT_TYPE_BRAINTREE_PAYPAL) { + $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(); @@ -204,17 +222,6 @@ class PaymentController extends BaseController $data['tokenize'] = true; } - } else { - if ($deviceData = Input::get('details')) { - 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) { - return Redirect::to('view/'.$invitationKey); - } - $data['paypalDetails'] = $paypalDetails; } $data += [ @@ -405,7 +412,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 +429,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 +480,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; + } } } @@ -482,7 +504,6 @@ class PaymentController extends BaseController ->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 @@ -494,6 +515,17 @@ class PaymentController extends BaseController $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; @@ -501,11 +533,6 @@ class PaymentController extends BaseController if ($accountGateway->gateway_id == GATEWAY_STRIPE) { $tokenBillingSupported = true; $customerReferenceParam = 'customerReference'; - - 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; $sourceReferenceParam = 'paymentMethodToken'; @@ -531,8 +558,8 @@ class PaymentController extends BaseController } $details[$sourceReferenceParam] = $sourceReference; unset($details['card']); - } elseif ($account->token_billing_type_id == TOKEN_BILLING_ALWAYS || Input::get('token_billing') || $paymentType == PAYMENT_TYPE_STRIPE_ACH) { - $token = $this->paymentService->createToken($gateway, $details, $accountGateway, $client, $invitation->contact_id, $customerReference/* return parameter */, $paymentMethod/* return parameter */); + } 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) { @@ -570,7 +597,7 @@ class PaymentController extends BaseController if (!$ref) { $this->error('No-Ref', $response->getMessage(), $accountGateway); - if ($onSite && $paymentType != PAYMENT_TYPE_BRAINTREE_PAYPAL) { + if ($onSite && $paymentType != PAYMENT_TYPE_BRAINTREE_PAYPAL && $paymentType != PAYMENT_TYPE_WEPAY_ACH) { return Redirect::to('payment/'.$invitationKey) ->withInput(Request::except('cvv')); } else { @@ -598,7 +625,7 @@ class PaymentController extends BaseController $response->redirect(); } else { $this->error('Unknown', $response->getMessage(), $accountGateway); - if ($onSite && $paymentType != PAYMENT_TYPE_BRAINTREE_PAYPAL) { + 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); @@ -606,7 +633,7 @@ class PaymentController extends BaseController } } catch (\Exception $e) { $this->error('Uncaught', false, $accountGateway, $e); - if ($onSite && $paymentType != PAYMENT_TYPE_BRAINTREE_PAYPAL) { + 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); @@ -667,7 +694,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/Http/Middleware/Authenticate.php b/app/Http/Middleware/Authenticate.php index 81fde62439e2..1b38969fe929 100644 --- a/app/Http/Middleware/Authenticate.php +++ b/app/Http/Middleware/Authenticate.php @@ -19,38 +19,59 @@ 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)){ - // This is a different client; reauthenticate - $authenticated = false; - Auth::guard($guard)->logout(); - } - } - Session::put('invitation_key', $request->invitation_key); - } - if($guard=='client'){ - $invitation_key = session('invitation_key'); - $account_id = $this->getInvitationAccountId($invitation_key); - - if(Auth::guard('user')->check() && Auth::user('user')->account_id === $account_id){ + 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); + } else { + $contact_key = session('contact_key'); + } + + if ($contact_key) { + $contact = $this->getContact($contact_key); + } elseif (!empty($request->invitation_key)) { + $invitation = $this->getInvitation($request->invitation_key); + $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; } // 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 +97,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 f8fcbca5ffd7..0db6eaaa6d2d 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')); @@ -673,6 +675,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 3248bf95814f..2ab3858d39ff 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -384,26 +384,31 @@ 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; + if ($type == PAYMENT_TYPE_WEPAY_ACH) { + return $this->getGatewayConfig(GATEWAY_WEPAY); } 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; } } @@ -1424,32 +1429,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/app/Models/Contact.php b/app/Models/Contact.php index 9c86c4ce5b84..e47211e5133e 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) { @@ -68,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/app/Models/Invoice.php b/app/Models/Invoice.php index 17b6292739c1..30e9a7f792a4 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -947,6 +947,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 59f1e1e9823e..288bc566a8f1 100644 --- a/app/Models/Payment.php +++ b/app/Models/Payment.php @@ -180,6 +180,16 @@ 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; diff --git a/app/Models/PaymentMethod.php b/app/Models/PaymentMethod.php index a2f17b722ae6..8c3ea8b2dfd8 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,10 @@ class PaymentMethod extends EntityModel return null; } } + + public function requiresDelayedAutoBill(){ + return $this->payment_type_id == PAYMENT_TYPE_ACH; + } } PaymentMethod::deleting(function($paymentMethod) { 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') { 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/Mailers/ContactMailer.php b/app/Ninja/Mailers/ContactMailer.php index 12662a606ed6..9fa2009a7b72 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/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/app/Ninja/Repositories/InvoiceRepository.php b/app/Ninja/Repositories/InvoiceRepository.php index d79c15f24c08..91ac711390bc 100644 --- a/app/Ninja/Repositories/InvoiceRepository.php +++ b/app/Ninja/Repositories/InvoiceRepository.php @@ -798,7 +798,8 @@ 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) { + // 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/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 b227b466413c..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; @@ -88,6 +89,10 @@ class PaymentService extends BaseService 'transactionType' => 'Purchase', ]; + if ($input !== null) { + $data['ip'] = \Request::ip(); + } + if ($accountGateway->isGateway(GATEWAY_PAYPAL_EXPRESS) || $accountGateway->isGateway(GATEWAY_PAYPAL_PRO)) { $data['ButtonSource'] = 'InvoiceNinja_SP'; }; @@ -302,7 +307,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 */); @@ -394,27 +399,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']; @@ -442,6 +456,8 @@ class PaymentService extends BaseService $accountGatewayToken->save(); $paymentMethod = $this->convertPaymentMethodFromGatewayResponse($tokenResponse, $accountGateway, $accountGatewayToken, $contactId); + $paymentMethod->ip = \Request::ip(); + $paymentMethod->save(); } else { $this->lastError = $tokenResponse->getMessage(); @@ -510,12 +526,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; } @@ -564,10 +597,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'], )); } @@ -644,6 +679,10 @@ class PaymentService extends BaseService $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 @@ -680,6 +719,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; } @@ -829,11 +872,46 @@ class PaymentService extends BaseService return false; } + if ($defaultPaymentMethod->requiresDelayedAutoBill()) { + $invoiceDate = \DateTime::createFromFormat('Y-m-d', $invoice->invoice_date); + $minDueDate = clone $invoiceDate; + $minDueDate->modify('+10 days'); + + if (date_create() < $minDueDate) { + // Can't auto bill now + return false; + } + + 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(); + + 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; + } + } + + if ($invoice->payments->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; $details['token'] = $defaultPaymentMethod->source_reference; + $details['paymentType'] = $defaultPaymentMethod->payment_type_id; if ($accountGateway->gateway_id == GATEWAY_WEPAY) { $details['transaction_id'] = 'autobill_'.$invoice->id; } @@ -849,6 +927,24 @@ 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 getClientRequiresDelayedAutoBill($client) { + $defaultPaymentMethod = $this->getClientDefaultPaymentMethod($client); + + return $defaultPaymentMethod?$defaultPaymentMethod->requiresDelayedAutoBill():null; + } + public function getDatatable($clientPublicId, $search) { $datatable = new PaymentDatatable( ! $clientPublicId, $clientPublicId); @@ -1057,6 +1153,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/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/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", 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..cb1b79433321 --- /dev/null +++ b/database/migrations/2016_05_24_164847_wepay_ach.php @@ -0,0 +1,62 @@ +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); + }); + } + + /** + * 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('bank_name'); + $table->dropColumn('ip'); + }); + + Schema::table('payment_methods', function($table) { + $table->dropColumn('bank_name'); + $table->dropColumn('ip'); + }); + + Schema::table('accounts', function($table) { + $table->dropColumn('auto_bill_on_due_date'); + }); + } +} diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 6d4fcdba26ec..093601aa7660 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -1203,7 +1203,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', @@ -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', @@ -1324,6 +1324,30 @@ $LANG = array( 'export_help' => 'Use JSON if you plan to import the data into Invoice Ninja.', 'JSON_file' => 'JSON File', + '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.', + + '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.', + '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', + '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.', + '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', ); return $LANG; diff --git a/resources/views/accounts/account_gateway.blade.php b/resources/views/accounts/account_gateway.blade.php index cfad9f21a6b6..f195d2849ab8 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)
@@ -118,7 +111,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')) @@ -154,33 +147,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 bdbe974aa5e9..a030d4c825b6 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')) !!} @@ -53,6 +49,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/accounts/payments.blade.php b/resources/views/accounts/payments.blade.php index 997190ec4f0a..603aec4bf6f5 100644 --- a/resources/views/accounts/payments.blade.php +++ b/resources/views/accounts/payments.blade.php @@ -4,6 +4,33 @@ @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::inline_radios('auto_bill_on_due_date') + ->label(trans('texts.auto_bill')) + ->radios([ + trans('texts.on_send_date') => ['value'=>0, 'name'=>'auto_bill_on_due_date'], + trans('texts.on_due_date') => ['value'=>1, 'name'=>'auto_bill_on_due_date'], + ])->help(trans('texts.auto_bill_ach_date_help')) !!} +
+

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

+
+ {!! 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')) 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 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', ]) diff --git a/resources/views/invoices/edit.blade.php b/resources/views/invoices/edit.blade.php index f32cdcdc70cf..42aa40d27457 100644 --- a/resources/views/invoices/edit.blade.php +++ b/resources/views/invoices/edit.blade.php @@ -1226,6 +1226,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' }}) { @@ -1355,6 +1359,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 ) }}'; } diff --git a/resources/views/invoices/view.blade.php b/resources/views/invoices/view.blade.php index 096b7b74f765..2db836ec78f4 100644 --- a/resources/views/invoices/view.blade.php +++ b/resources/views/invoices/view.blade.php @@ -59,6 +59,35 @@ }) }); + @elseif(!empty($enableWePayACH)) + + @endif @stop diff --git a/resources/views/payments/add_paymentmethod.blade.php b/resources/views/payments/add_paymentmethod.blade.php index 6d4e9ff24185..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($gateway->gateway_id == GATEWAY_WEPAY && $gateway->getAchEnabled()) + + @endif @if(!empty($paymentMethods)) @foreach ($paymentMethods as $paymentMethod) @@ -59,11 +85,15 @@ •••••{{$paymentMethod->last4}} @endif @if($paymentMethod->payment_type_id == PAYMENT_TYPE_ACH) - @if($paymentMethod->bank_data) - {{ $paymentMethod->bank_data->name }} + @if($paymentMethod->bank_name) + {{ $paymentMethod->bank_name }} @endif @if($paymentMethod->status == PAYMENT_METHOD_STATUS_NEW) - ({{trans('texts.complete_verification')}}) + @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 @@ -88,8 +118,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())