diff --git a/app/DataProviders/USStates.php b/app/DataProviders/USStates.php new file mode 100644 index 000000000000..0551b5fbc301 --- /dev/null +++ b/app/DataProviders/USStates.php @@ -0,0 +1,75 @@ + 'Alabama', + 'AK' => 'Alaska', + 'AZ' => 'Arizona', + 'AR' => 'Arkansas', + 'CA' => 'California', + 'CO' => 'Colorado', + 'CT' => 'Connecticut', + 'DE' => 'Delaware', + 'DC' => 'District Of Columbia', + 'FL' => 'Florida', + 'GA' => 'Georgia', + 'HI' => 'Hawaii', + 'ID' => 'Idaho', + 'IL' => 'Illinois', + 'IN' => 'Indiana', + 'IA' => 'Iowa', + 'KS' => 'Kansas', + 'KY' => 'Kentucky', + 'LA' => 'Louisiana', + 'ME' => 'Maine', + 'MD' => 'Maryland', + 'MA' => 'Massachusetts', + 'MI' => 'Michigan', + 'MN' => 'Minnesota', + 'MS' => 'Mississippi', + 'MO' => 'Missouri', + 'MT' => 'Montana', + 'NE' => 'Nebraska', + 'NV' => 'Nevada', + 'NH' => 'New Hampshire', + 'NJ' => 'New Jersey', + 'NM' => 'New Mexico', + 'NY' => 'New York', + 'NC' => 'North Carolina', + 'ND' => 'North Dakota', + 'OH' => 'Ohio', + 'OK' => 'Oklahoma', + 'OR' => 'Oregon', + 'PA' => 'Pennsylvania', + 'RI' => 'Rhode Island', + 'SC' => 'South Carolina', + 'SD' => 'South Dakota', + 'TN' => 'Tennessee', + 'TX' => 'Texas', + 'UT' => 'Utah', + 'VT' => 'Vermont', + 'VA' => 'Virginia', + 'WA' => 'Washington', + 'WV' => 'West Virginia', + 'WI' => 'Wisconsin', + 'WY' => 'Wyoming', + ]; + + public static function get(): array + { + return self::$states; + } +} diff --git a/app/Models/Gateway.php b/app/Models/Gateway.php index 676112f97911..dadb195e52fb 100644 --- a/app/Models/Gateway.php +++ b/app/Models/Gateway.php @@ -83,7 +83,7 @@ class Gateway extends StaticModel break; case 3: return [GatewayType::CREDIT_CARD => ['refund' => false, 'token_billing' => true]];//eWay - break; + break; case 11: return [GatewayType::CREDIT_CARD => ['refund' => false, 'token_billing' => false]];//Payfast break; @@ -106,11 +106,12 @@ class Gateway extends StaticModel case 49: return [GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true], GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true]]; //WePay - break; + break; case 50: return [ GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true], //Braintree - GatewayType::PAYPAL => ['refund' => true, 'token_billing' => true] + GatewayType::PAYPAL => ['refund' => true, 'token_billing' => true], + GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true], ]; break; case 7: diff --git a/app/PaymentDrivers/BaseDriver.php b/app/PaymentDrivers/BaseDriver.php index 65705fc19c9d..b984b2b41eaa 100644 --- a/app/PaymentDrivers/BaseDriver.php +++ b/app/PaymentDrivers/BaseDriver.php @@ -360,16 +360,15 @@ class BaseDriver extends AbstractPaymentDriver public function processInternallyFailedPayment($gateway, $e) { - - $this->unWindGatewayFees($this->payment_hash); + if (!is_null($this->payment_hash)) { + $this->unWindGatewayFees($this->payment_hash); + } if ($e instanceof CheckoutHttpException) { $error = $e->getBody(); - } - else if ($e instanceof Exception) { + } else if ($e instanceof Exception) { $error = $e->getMessage(); - } - else + } else $error = $e->getMessage(); PaymentFailureMailer::dispatch( @@ -379,29 +378,29 @@ class BaseDriver extends AbstractPaymentDriver $this->payment_hash ); - $nmo = new NinjaMailerObject; - $nmo->mailable = new NinjaMailer( (new ClientPaymentFailureObject($gateway->client, $error, $gateway->client->company, $this->payment_hash))->build() ); - $nmo->company = $gateway->client->company; - $nmo->settings = $gateway->client->company->settings; + if (!is_null($this->payment_hash)) { - $invoices = Invoice::whereIn('id', $this->transformKeys(array_column($this->payment_hash->invoices(), 'invoice_id')))->withTrashed()->get(); + $nmo = new NinjaMailerObject; + $nmo->mailable = new NinjaMailer((new ClientPaymentFailureObject($gateway->client, $error, $gateway->client->company, $this->payment_hash))->build()); + $nmo->company = $gateway->client->company; + $nmo->settings = $gateway->client->company->settings; - $invoices->each(function ($invoice){ + $invoices = Invoice::whereIn('id', $this->transformKeys(array_column($this->payment_hash->invoices(), 'invoice_id')))->withTrashed()->get(); - $invoice->service()->deletePdf(); + $invoices->each(function ($invoice) { - }); + $invoice->service()->deletePdf(); + }); - $invoices->first()->invitations->each(function ($invitation) use ($nmo){ + $invoices->first()->invitations->each(function ($invitation) use ($nmo) { - if ($invitation->contact->send_email && $invitation->contact->email) { + if ($invitation->contact->send_email && $invitation->contact->email) { - $nmo->to_user = $invitation->contact; - NinjaMailerJob::dispatch($nmo); - - } - - }); + $nmo->to_user = $invitation->contact; + NinjaMailerJob::dispatch($nmo); + } + }); + } SystemLogger::dispatch( diff --git a/app/PaymentDrivers/Braintree/ACH.php b/app/PaymentDrivers/Braintree/ACH.php new file mode 100644 index 000000000000..1a91473d5b07 --- /dev/null +++ b/app/PaymentDrivers/Braintree/ACH.php @@ -0,0 +1,186 @@ +braintree = $braintree; + + $this->braintree->init(); + } + + public function authorizeView(array $data) + { + $data['gateway'] = $this->braintree; + $data['client_token'] = $this->braintree->gateway->clientToken()->generate(); + + return render('gateways.braintree.ach.authorize', $data); + } + + public function authorizeResponse(Request $request) + { + $request->validate([ + 'nonce' => ['required'], + 'gateway_type_id' => ['required'], + ]); + + $customer = $this->braintree->findOrCreateCustomer(); + + $result = $this->braintree->gateway->paymentMethod()->create([ + 'customerId' => $customer->id, + 'paymentMethodNonce' => $request->nonce, + 'options' => [ + 'usBankAccountVerificationMethod' => \Braintree\Result\UsBankAccountVerification::NETWORK_CHECK, + ], + ]); + + if ($result->success && optional($result->paymentMethod)->verified) { + $account = $result->paymentMethod; + + try { + $payment_meta = new \stdClass; + $payment_meta->brand = (string)$account->bankName; + $payment_meta->last4 = (string)$account->last4; + $payment_meta->type = GatewayType::BANK_TRANSFER; + $payment_meta->state = 'authorized'; + + $data = [ + 'payment_meta' => $payment_meta, + 'token' => $account->token, + 'payment_method_id' => $request->gateway_type_id, + ]; + + $this->braintree->storeGatewayToken($data, ['gateway_customer_reference' => $customer->id]); + + return redirect()->route('client.payment_methods.index')->withMessage(ctrans('texts.payment_method_added')); + } catch (\Exception $e) { + return $this->braintree->processInternallyFailedPayment($this->braintree, $e); + } + } + + return back()->withMessage(ctrans('texts.unable_to_verify_payment_method')); + } + + public function paymentView(array $data) + { + $data['gateway'] = $this->braintree; + $data['currency'] = $this->braintree->client->getCurrencyCode(); + $data['payment_method_id'] = GatewayType::BANK_TRANSFER; + $data['amount'] = $this->braintree->payment_hash->data->amount_with_fee; + + return render('gateways.braintree.ach.pay', $data); + } + + public function paymentResponse(PaymentResponseRequest $request) + { + $request->validate([ + 'source' => ['required'], + 'payment_hash' => ['required'], + ]); + + $customer = $this->braintree->findOrCreateCustomer(); + + $token = ClientGatewayToken::query() + ->where('client_id', auth('contact')->user()->client->id) + ->where('id', $this->decodePrimaryKey($request->source)) + ->firstOrFail(); + + $result = $this->braintree->gateway->transaction()->sale([ + 'amount' => $this->braintree->payment_hash->data->amount_with_fee, + 'paymentMethodToken' => $token->token, + 'options' => [ + 'submitForSettlement' => true + ], + ]); + + if ($result->success) { + $this->braintree->logSuccessfulGatewayResponse(['response' => $request->server_response, 'data' => $this->braintree->payment_hash], SystemLog::TYPE_BRAINTREE); + + return $this->processSuccessfulPayment($result); + } + + return $this->processUnsuccessfulPayment($result); + } + + private function processSuccessfulPayment($response) + { + $state = $this->braintree->payment_hash->data; + + $data = [ + 'payment_type' => PaymentType::ACH, + 'amount' => $this->braintree->payment_hash->data->amount_with_fee, + 'transaction_reference' => $response->transaction->id, + 'gateway_type_id' => GatewayType::BANK_TRANSFER, + ]; + + $payment = $this->braintree->createPayment($data, Payment::STATUS_COMPLETED); + + SystemLogger::dispatch( + ['response' => $response, 'data' => $data], + SystemLog::CATEGORY_GATEWAY_RESPONSE, + SystemLog::EVENT_GATEWAY_SUCCESS, + SystemLog::TYPE_BRAINTREE, + $this->braintree->client, + $this->braintree->client->company, + ); + + return redirect()->route('client.payments.show', ['payment' => $this->braintree->encodePrimaryKey($payment->id)]); + } + + private function processUnsuccessfulPayment($response) + { + PaymentFailureMailer::dispatch($this->braintree->client, $response->transaction->additionalProcessorResponse, $this->braintree->client->company, $this->braintree->payment_hash->data->amount_with_fee); + + PaymentFailureMailer::dispatch( + $this->braintree->client, + $response, + $this->braintree->client->company, + $this->braintree->payment_hash->data->amount_with_fee, + ); + + $message = [ + 'server_response' => $response, + 'data' => $this->braintree->payment_hash->data, + ]; + + SystemLogger::dispatch( + $message, + SystemLog::CATEGORY_GATEWAY_RESPONSE, + SystemLog::EVENT_GATEWAY_FAILURE, + SystemLog::TYPE_BRAINTREE, + $this->braintree->client, + $this->braintree->client->company, + ); + + throw new PaymentFailed($response->transaction->additionalProcessorResponse, $response->transaction->processorResponseCode); + } +} diff --git a/app/PaymentDrivers/BraintreePaymentDriver.php b/app/PaymentDrivers/BraintreePaymentDriver.php index 7c0cdd278685..f1d0858284cf 100644 --- a/app/PaymentDrivers/BraintreePaymentDriver.php +++ b/app/PaymentDrivers/BraintreePaymentDriver.php @@ -23,6 +23,7 @@ use App\Models\Payment; use App\Models\PaymentHash; use App\Models\PaymentType; use App\Models\SystemLog; +use App\PaymentDrivers\Braintree\ACH; use App\PaymentDrivers\Braintree\CreditCard; use App\PaymentDrivers\Braintree\PayPal; use Braintree\Gateway; @@ -45,6 +46,7 @@ class BraintreePaymentDriver extends BaseDriver public static $methods = [ GatewayType::CREDIT_CARD => CreditCard::class, GatewayType::PAYPAL => PayPal::class, + GatewayType::BANK_TRANSFER => ACH::class, ]; const SYSTEM_LOG_TYPE = SystemLog::TYPE_BRAINTREE; @@ -72,9 +74,10 @@ class BraintreePaymentDriver extends BaseDriver { $types = [ GatewayType::PAYPAL, - GatewayType::CREDIT_CARD + GatewayType::CREDIT_CARD, + GatewayType::BANK_TRANSFER, ]; - + return $types; } @@ -125,9 +128,9 @@ class BraintreePaymentDriver extends BaseDriver $this->init(); try{ - + $response = $this->gateway->transaction()->refund($payment->transaction_reference, $amount); - + } catch (Exception $e) { $data = [ @@ -137,12 +140,12 @@ class BraintreePaymentDriver extends BaseDriver 'description' => $e->getMessage(), 'code' => $e->getCode(), ]; - + SystemLogger::dispatch(['server_response' => null, 'data' => $data], SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_BRAINTREE, $this->client, $this->client->company); return $data; } - + if($response->success) { @@ -218,7 +221,8 @@ class BraintreePaymentDriver extends BaseDriver SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::TYPE_BRAINTREE, - $this->client + $this->client, + $this->client->company, ); return $payment; @@ -239,7 +243,8 @@ class BraintreePaymentDriver extends BaseDriver SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_BRAINTREE, - $this->client + $this->client, + $this->client->company ); return false; diff --git a/app/PaymentDrivers/Common/MethodInterface.php b/app/PaymentDrivers/Common/MethodInterface.php new file mode 100644 index 000000000000..0c839d602cdd --- /dev/null +++ b/app/PaymentDrivers/Common/MethodInterface.php @@ -0,0 +1,46 @@ + { + e.target.parentElement.disabled = true; + + document.getElementById('errors').hidden = true; + document.getElementById('errors').textContent = ''; + + let bankDetails = { + accountNumber: document.getElementById('account-number').value, + routingNumber: document.getElementById('routing-number').value, + accountType: document.querySelector('input[name="account-type"]:checked').value, + ownershipType: document.querySelector('input[name="ownership-type"]:checked').value, + billingAddress: { + streetAddress: document.getElementById('billing-street-address').value, + extendedAddress: document.getElementById('billing-extended-address').value, + locality: document.getElementById('billing-locality').value, + region: document.getElementById('billing-region').value, + postalCode: document.getElementById('billing-postal-code').value + } + } + + if (bankDetails.ownershipType === 'personal') { + let name = document.getElementById('account-holder-name').value.split(' ', 2); + + bankDetails.firstName = name[0]; + bankDetails.lastName = name[1]; + } else { + bankDetails.businessName = document.getElementById('account-holder-name').value; + } + + usBankAccountInstance.tokenize({ + bankDetails, + mandateText: 'By clicking ["Checkout"], I authorize Braintree, a service of PayPal, on behalf of [your business name here] (i) to verify my bank account information using bank information and consumer reports and (ii) to debit my bank account.' + }).then(function (payload) { + document.querySelector('input[name=nonce]').value = payload.nonce; + document.getElementById('server_response').submit(); + }) + .catch(function (error) { + e.target.parentElement.disabled = false; + + document.getElementById('errors').textContent = `${error.details.originalError.message} ${error.details.originalError.details.originalError[0].message}`; + document.getElementById('errors').hidden = false; + }); + }); +}).catch(function (err) { + document.getElementById('errors').textContent = `${error.details.originalError.message} ${error.details.originalError.details.originalError[0].message}`; + document.getElementById('errors').hidden = false; +}); diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 18fd55344cb3..7ed8c1b22ec1 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -4297,7 +4297,11 @@ $LANG = array( 'lang_Latvian' => 'Latvian', 'expiry_date' => 'Expiry date', 'cardholder_name' => 'Card holder name', - + 'account_type' => 'Account type', + 'locality' => 'Locality', + 'checking' => 'Checking', + 'savings' => 'Savings', + 'unable_to_verify_payment_method' => 'Unable to verify payment method.', ); return $LANG; diff --git a/resources/views/portal/ninja2020/gateways/braintree/ach/authorize.blade.php b/resources/views/portal/ninja2020/gateways/braintree/ach/authorize.blade.php new file mode 100644 index 000000000000..91e313fc277f --- /dev/null +++ b/resources/views/portal/ninja2020/gateways/braintree/ach/authorize.blade.php @@ -0,0 +1,95 @@ +@extends('portal.ninja2020.layout.payments', ['gateway_title' => 'ACH', 'card_title' => 'ACH']) + +@section('gateway_head') + +@endsection + +@section('gateway_content') + @if(session()->has('ach_error')) +
{{ session('ach_error') }}
+