From 5ff69c6f295a4c05babb50b84823f3cf0ff090d9 Mon Sep 17 00:00:00 2001 From: Hillel Coren Date: Mon, 20 Jun 2016 17:14:43 +0300 Subject: [PATCH] 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))