mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-07-09 03:14:30 -04:00
Merge pull request #5565 from beganovich/v5-2804-braintree
v5: Braintree integration
This commit is contained in:
commit
6d299c9e53
@ -243,7 +243,7 @@ class PaymentController extends Controller
|
|||||||
->get();
|
->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
$hash_data = ['invoices' => $payable_invoices->toArray(), 'credits' => $credit_totals];
|
$hash_data = ['invoices' => $payable_invoices->toArray(), 'credits' => $credit_totals, 'amount_with_fee' => max(0, (($invoice_totals + $fee_totals) - $credit_totals))];
|
||||||
|
|
||||||
if ($request->query('hash')) {
|
if ($request->query('hash')) {
|
||||||
$hash_data['billing_context'] = Cache::get($request->query('hash'));
|
$hash_data['billing_context'] = Cache::get($request->query('hash'));
|
||||||
@ -303,24 +303,12 @@ class PaymentController extends Controller
|
|||||||
|
|
||||||
$payment_hash = PaymentHash::whereRaw('BINARY `hash`= ?', [$request->payment_hash])->first();
|
$payment_hash = PaymentHash::whereRaw('BINARY `hash`= ?', [$request->payment_hash])->first();
|
||||||
|
|
||||||
try {
|
|
||||||
return $gateway
|
return $gateway
|
||||||
->driver(auth()->user()->client)
|
->driver(auth()->user()->client)
|
||||||
->setPaymentMethod($request->input('payment_method_id'))
|
->setPaymentMethod($request->input('payment_method_id'))
|
||||||
->setPaymentHash($payment_hash)
|
->setPaymentHash($payment_hash)
|
||||||
->checkRequirements()
|
->checkRequirements()
|
||||||
->processPaymentResponse($request);
|
->processPaymentResponse($request);
|
||||||
} catch (\Exception $e) {
|
|
||||||
SystemLogger::dispatch(
|
|
||||||
$e->getMessage(),
|
|
||||||
SystemLog::CATEGORY_GATEWAY_RESPONSE,
|
|
||||||
SystemLog::EVENT_GATEWAY_FAILURE,
|
|
||||||
SystemLog::TYPE_FAILURE,
|
|
||||||
auth('contact')->user()->client
|
|
||||||
);
|
|
||||||
|
|
||||||
throw new PaymentFailed($e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -95,6 +95,12 @@ class Gateway extends StaticModel
|
|||||||
case 39:
|
case 39:
|
||||||
return [GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true]]; //Checkout
|
return [GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true]]; //Checkout
|
||||||
break;
|
break;
|
||||||
|
case 50:
|
||||||
|
return [
|
||||||
|
GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true],
|
||||||
|
GatewayType::PAYPAL => ['refund' => true, 'token_billing' => true]
|
||||||
|
];
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
return [];
|
return [];
|
||||||
break;
|
break;
|
||||||
|
@ -65,7 +65,8 @@ class SystemLog extends Model
|
|||||||
const TYPE_CHECKOUT = 304;
|
const TYPE_CHECKOUT = 304;
|
||||||
const TYPE_AUTHORIZE = 305;
|
const TYPE_AUTHORIZE = 305;
|
||||||
const TYPE_CUSTOM = 306;
|
const TYPE_CUSTOM = 306;
|
||||||
|
const TYPE_BRAINTREE = 307;
|
||||||
|
|
||||||
const TYPE_QUOTA_EXCEEDED = 400;
|
const TYPE_QUOTA_EXCEEDED = 400;
|
||||||
const TYPE_UPSTREAM_FAILURE = 401;
|
const TYPE_UPSTREAM_FAILURE = 401;
|
||||||
|
|
||||||
|
209
app/PaymentDrivers/Braintree/CreditCard.php
Normal file
209
app/PaymentDrivers/Braintree/CreditCard.php
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://opensource.org/licenses/AAL
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\PaymentDrivers\Braintree;
|
||||||
|
|
||||||
|
|
||||||
|
use App\Exceptions\PaymentFailed;
|
||||||
|
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
|
||||||
|
use App\Http\Requests\Request;
|
||||||
|
use App\Jobs\Mail\PaymentFailureMailer;
|
||||||
|
use App\Jobs\Util\SystemLogger;
|
||||||
|
use App\Models\GatewayType;
|
||||||
|
use App\Models\Payment;
|
||||||
|
use App\Models\PaymentType;
|
||||||
|
use App\Models\SystemLog;
|
||||||
|
use App\PaymentDrivers\BraintreePaymentDriver;
|
||||||
|
|
||||||
|
class CreditCard
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var BraintreePaymentDriver
|
||||||
|
*/
|
||||||
|
private $braintree;
|
||||||
|
|
||||||
|
public function __construct(BraintreePaymentDriver $braintree)
|
||||||
|
{
|
||||||
|
$this->braintree = $braintree;
|
||||||
|
|
||||||
|
$this->braintree->init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function authorizeView(array $data)
|
||||||
|
{
|
||||||
|
$data['gateway'] = $this->braintree;
|
||||||
|
|
||||||
|
return render('gateways.braintree.credit_card.authorize', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function authorizeResponse($data): \Illuminate\Http\RedirectResponse
|
||||||
|
{
|
||||||
|
return back();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Credit card payment page.
|
||||||
|
*
|
||||||
|
* @param array $data
|
||||||
|
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||||
|
*/
|
||||||
|
public function paymentView(array $data)
|
||||||
|
{
|
||||||
|
$data['gateway'] = $this->braintree;
|
||||||
|
$data['client_token'] = $this->braintree->gateway->clientToken()->generate();
|
||||||
|
|
||||||
|
return render('gateways.braintree.credit_card.pay', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process the credit card payments.
|
||||||
|
*
|
||||||
|
* @param PaymentResponseRequest $request
|
||||||
|
* @return \Illuminate\Http\RedirectResponse|void
|
||||||
|
* @throws PaymentFailed
|
||||||
|
*/
|
||||||
|
public function paymentResponse(PaymentResponseRequest $request)
|
||||||
|
{
|
||||||
|
$state = [
|
||||||
|
'server_response' => json_decode($request->gateway_response),
|
||||||
|
'payment_hash' => $request->payment_hash,
|
||||||
|
];
|
||||||
|
|
||||||
|
$state = array_merge($state, $request->all());
|
||||||
|
$state['store_card'] = boolval($state['store_card']);
|
||||||
|
|
||||||
|
$this->braintree->payment_hash->data = array_merge((array)$this->braintree->payment_hash->data, $state);
|
||||||
|
$this->braintree->payment_hash->save();
|
||||||
|
|
||||||
|
$customer = $this->braintree->findOrCreateCustomer();
|
||||||
|
|
||||||
|
$token = $this->getPaymentToken($request->all(), $customer->id);
|
||||||
|
|
||||||
|
$result = $this->braintree->gateway->transaction()->sale([
|
||||||
|
'amount' => $this->braintree->payment_hash->data->amount_with_fee,
|
||||||
|
'paymentMethodToken' => $token,
|
||||||
|
'deviceData' => $state['client-data'],
|
||||||
|
'options' => [
|
||||||
|
'submitForSettlement' => true
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($result->success) {
|
||||||
|
$this->braintree->logSuccessfulGatewayResponse(['response' => $request->server_response, 'data' => $this->braintree->payment_hash], SystemLog::TYPE_BRAINTREE);
|
||||||
|
|
||||||
|
if ($request->store_card && is_null($request->token)) {
|
||||||
|
$payment_method = $this->braintree->gateway->paymentMethod()->find($token);
|
||||||
|
|
||||||
|
$this->storePaymentMethod($payment_method, $customer->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->processSuccessfulPayment($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->processUnsuccessfulPayment($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getPaymentToken(array $data, $customerId): ?string
|
||||||
|
{
|
||||||
|
if (array_key_exists('token', $data) && !is_null($data['token'])) {
|
||||||
|
return $data['token'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$gateway_response = json_decode($data['gateway_response']);
|
||||||
|
|
||||||
|
$payment_method = $this->braintree->gateway->paymentMethod()->create([
|
||||||
|
'customerId' => $customerId,
|
||||||
|
'paymentMethodNonce' => $gateway_response->nonce,
|
||||||
|
'options' => [
|
||||||
|
'verifyCard' => true,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $payment_method->paymentMethod->token;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processSuccessfulPayment($response)
|
||||||
|
{
|
||||||
|
$state = $this->braintree->payment_hash->data;
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'payment_type' => PaymentType::parseCardType(strtolower($response->transaction->creditCard['cardType'])),
|
||||||
|
'amount' => $this->braintree->payment_hash->data->amount_with_fee,
|
||||||
|
'transaction_reference' => $response->transaction->id,
|
||||||
|
'gateway_type_id' => GatewayType::CREDIT_CARD,
|
||||||
|
];
|
||||||
|
|
||||||
|
$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
|
||||||
|
);
|
||||||
|
|
||||||
|
return redirect()->route('client.payments.show', ['payment' => $this->braintree->encodePrimaryKey($payment->id)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws PaymentFailed
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
);
|
||||||
|
|
||||||
|
throw new PaymentFailed($response->transaction->additionalProcessorResponse, $response->transaction->processorResponseCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function storePaymentMethod($method, $customer_reference)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$payment_meta = new \stdClass;
|
||||||
|
$payment_meta->exp_month = (string)$method->expirationMonth;
|
||||||
|
$payment_meta->exp_year = (string)$method->expirationYear;
|
||||||
|
$payment_meta->brand = (string)$method->cardType;
|
||||||
|
$payment_meta->last4 = (string)$method->last4;
|
||||||
|
$payment_meta->type = GatewayType::CREDIT_CARD;
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'payment_meta' => $payment_meta,
|
||||||
|
'token' => $method->token,
|
||||||
|
'payment_method_id' => $this->braintree->payment_hash->data->payment_method_id,
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->braintree->storeGatewayToken($data, ['gateway_customer_reference' => $customer_reference]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->braintree->processInternallyFailedPayment($this->braintree, $e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
195
app/PaymentDrivers/Braintree/PayPal.php
Normal file
195
app/PaymentDrivers/Braintree/PayPal.php
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace App\PaymentDrivers\Braintree;
|
||||||
|
|
||||||
|
|
||||||
|
use App\Exceptions\PaymentFailed;
|
||||||
|
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
|
||||||
|
use App\Jobs\Mail\PaymentFailureMailer;
|
||||||
|
use App\Jobs\Util\SystemLogger;
|
||||||
|
use App\Models\GatewayType;
|
||||||
|
use App\Models\Payment;
|
||||||
|
use App\Models\PaymentType;
|
||||||
|
use App\Models\SystemLog;
|
||||||
|
use App\PaymentDrivers\BraintreePaymentDriver;
|
||||||
|
|
||||||
|
class PayPal
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var BraintreePaymentDriver
|
||||||
|
*/
|
||||||
|
private $braintree;
|
||||||
|
|
||||||
|
public function __construct(BraintreePaymentDriver $braintree)
|
||||||
|
{
|
||||||
|
$this->braintree = $braintree;
|
||||||
|
|
||||||
|
$this->braintree->init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function authorizeView(array $data)
|
||||||
|
{
|
||||||
|
$data['gateway'] = $this->braintree;
|
||||||
|
|
||||||
|
return render('gateways.braintree.paypal.authorize', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function authorizeResponse($data): \Illuminate\Http\RedirectResponse
|
||||||
|
{
|
||||||
|
return back();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Credit card payment page.
|
||||||
|
*
|
||||||
|
* @param array $data
|
||||||
|
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||||
|
*/
|
||||||
|
public function paymentView(array $data)
|
||||||
|
{
|
||||||
|
$data['gateway'] = $this->braintree;
|
||||||
|
$data['client_token'] = $this->braintree->gateway->clientToken()->generate();
|
||||||
|
|
||||||
|
return render('gateways.braintree.paypal.pay', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function paymentResponse(PaymentResponseRequest $request)
|
||||||
|
{
|
||||||
|
$state = [
|
||||||
|
'server_response' => json_decode($request->gateway_response),
|
||||||
|
'payment_hash' => $request->payment_hash,
|
||||||
|
];
|
||||||
|
|
||||||
|
$state = array_merge($state, $request->all());
|
||||||
|
$state['store_card'] = boolval($state['store_card']);
|
||||||
|
|
||||||
|
$this->braintree->payment_hash->data = array_merge((array)$this->braintree->payment_hash->data, $state);
|
||||||
|
$this->braintree->payment_hash->save();
|
||||||
|
|
||||||
|
$customer = $this->braintree->findOrCreateCustomer();
|
||||||
|
|
||||||
|
$token = $this->getPaymentToken($request->all(), $customer->id);
|
||||||
|
|
||||||
|
$result = $this->braintree->gateway->transaction()->sale([
|
||||||
|
'amount' => $this->braintree->payment_hash->data->amount_with_fee,
|
||||||
|
'paymentMethodToken' => $token,
|
||||||
|
'deviceData' => $state['client-data'],
|
||||||
|
'options' => [
|
||||||
|
'submitForSettlement' => True,
|
||||||
|
'paypal' => [
|
||||||
|
'description' => 'Meaningful description.',
|
||||||
|
]
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($result->success) {
|
||||||
|
$this->braintree->logSuccessfulGatewayResponse(
|
||||||
|
['response' => $request->server_response, 'data' => $this->braintree->payment_hash],
|
||||||
|
SystemLog::TYPE_BRAINTREE
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($request->store_card && is_null($request->token)) {
|
||||||
|
$payment_method = $this->braintree->gateway->paymentMethod()->find($token);
|
||||||
|
|
||||||
|
$this->storePaymentMethod($payment_method, $customer->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->processSuccessfulPayment($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->processUnsuccessfulPayment($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getPaymentToken(array $data, string $customerId)
|
||||||
|
{
|
||||||
|
if (array_key_exists('token', $data) && !is_null($data['token'])) {
|
||||||
|
return $data['token'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$gateway_response = json_decode($data['gateway_response']);
|
||||||
|
|
||||||
|
$payment_method = $this->braintree->gateway->paymentMethod()->create([
|
||||||
|
'customerId' => $customerId,
|
||||||
|
'paymentMethodNonce' => $gateway_response->nonce,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $payment_method->paymentMethod->token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process & complete the successful PayPal transaction.
|
||||||
|
*
|
||||||
|
* @param $response
|
||||||
|
* @return \Illuminate\Http\RedirectResponse
|
||||||
|
*/
|
||||||
|
private function processSuccessfulPayment($response): \Illuminate\Http\RedirectResponse
|
||||||
|
{
|
||||||
|
$state = $this->braintree->payment_hash->data;
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'payment_type' => PaymentType::PAYPAL,
|
||||||
|
'amount' => $this->braintree->payment_hash->data->amount_with_fee,
|
||||||
|
'transaction_reference' => $response->transaction->id,
|
||||||
|
'gateway_type_id' => GatewayType::PAYPAL,
|
||||||
|
];
|
||||||
|
|
||||||
|
$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
|
||||||
|
);
|
||||||
|
|
||||||
|
return redirect()->route('client.payments.show', ['payment' => $this->braintree->encodePrimaryKey($payment->id)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function processUnsuccessfulPayment($response)
|
||||||
|
{
|
||||||
|
PaymentFailureMailer::dispatch($this->braintree->client, $response->message, $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
|
||||||
|
);
|
||||||
|
|
||||||
|
throw new PaymentFailed($response->message, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function storePaymentMethod($method, string $customer_reference)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$payment_meta = new \stdClass;
|
||||||
|
$payment_meta->email = (string)$method->email;
|
||||||
|
$payment_meta->type = GatewayType::PAYPAL;
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'payment_meta' => $payment_meta,
|
||||||
|
'token' => $method->token,
|
||||||
|
'payment_method_id' => $this->braintree->payment_hash->data->payment_method_id,
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->braintree->storeGatewayToken($data, ['gateway_customer_reference' => $customer_reference]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->braintree->processInternallyFailedPayment($this->braintree, $e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
211
app/PaymentDrivers/BraintreePaymentDriver.php
Normal file
211
app/PaymentDrivers/BraintreePaymentDriver.php
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://opensource.org/licenses/AAL
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\PaymentDrivers;
|
||||||
|
|
||||||
|
|
||||||
|
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
|
||||||
|
use App\Jobs\Mail\PaymentFailureMailer;
|
||||||
|
use App\Jobs\Util\SystemLogger;
|
||||||
|
use App\Models\ClientGatewayToken;
|
||||||
|
use App\Models\GatewayType;
|
||||||
|
use App\Models\Invoice;
|
||||||
|
use App\Models\Payment;
|
||||||
|
use App\Models\PaymentHash;
|
||||||
|
use App\Models\PaymentType;
|
||||||
|
use App\Models\SystemLog;
|
||||||
|
use App\PaymentDrivers\Braintree\CreditCard;
|
||||||
|
use App\PaymentDrivers\Braintree\PayPal;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class BraintreePaymentDriver extends BaseDriver
|
||||||
|
{
|
||||||
|
public $refundable = true;
|
||||||
|
|
||||||
|
public $token_billing = true;
|
||||||
|
|
||||||
|
public $can_authorise_credit_card = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \Braintree\Gateway;
|
||||||
|
*/
|
||||||
|
public $gateway;
|
||||||
|
|
||||||
|
public static $methods = [
|
||||||
|
GatewayType::CREDIT_CARD => CreditCard::class,
|
||||||
|
GatewayType::PAYPAL => PayPal::class,
|
||||||
|
];
|
||||||
|
|
||||||
|
const SYSTEM_LOG_TYPE = SystemLog::TYPE_BRAINTREE;
|
||||||
|
|
||||||
|
public function init(): void
|
||||||
|
{
|
||||||
|
$this->gateway = new \Braintree\Gateway([
|
||||||
|
'environment' => $this->company_gateway->getConfigField('testMode') ? 'sandbox' : 'production',
|
||||||
|
'merchantId' => $this->company_gateway->getConfigField('merchantId'),
|
||||||
|
'publicKey' => $this->company_gateway->getConfigField('publicKey'),
|
||||||
|
'privateKey' => $this->company_gateway->getConfigField('privateKey'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPaymentMethod($payment_method_id)
|
||||||
|
{
|
||||||
|
$class = self::$methods[$payment_method_id];
|
||||||
|
|
||||||
|
$this->payment_method = new $class($this);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function gatewayTypes(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
GatewayType::CREDIT_CARD,
|
||||||
|
GatewayType::PAYPAL,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function authorizeView($data)
|
||||||
|
{
|
||||||
|
return $this->payment_method->authorizeView($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function authorizeResponse($data)
|
||||||
|
{
|
||||||
|
return $this->payment_method->authorizeResponse($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function processPaymentView(array $data)
|
||||||
|
{
|
||||||
|
return $this->payment_method->paymentView($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function processPaymentResponse($request)
|
||||||
|
{
|
||||||
|
return $this->payment_method->paymentResponse($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findOrCreateCustomer()
|
||||||
|
{
|
||||||
|
$existing = ClientGatewayToken::query()
|
||||||
|
->where('company_gateway_id', $this->company_gateway->id)
|
||||||
|
->where('client_id', $this->client->id)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
return $this->gateway->customer()->find($existing->gateway_customer_reference);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->gateway->customer()->create([
|
||||||
|
'firstName' => $this->client->present()->name,
|
||||||
|
'email' => $this->client->present()->email,
|
||||||
|
'phone' => $this->client->present()->phone,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($result->success) {
|
||||||
|
return $result->customer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function refund(Payment $payment, $amount, $return_client_response = false)
|
||||||
|
{
|
||||||
|
$this->init();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = $this->gateway->transaction()->refund($payment->transaction_reference, $amount);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'transaction_reference' => $response->id,
|
||||||
|
'transaction_response' => json_encode($response),
|
||||||
|
'success' => (bool) $response->success,
|
||||||
|
'description' => $response->status,
|
||||||
|
'code' => 0,
|
||||||
|
];
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return [
|
||||||
|
'transaction_reference' => null,
|
||||||
|
'transaction_response' => json_encode($e->getMessage()),
|
||||||
|
'success' => false,
|
||||||
|
'description' => $e->getMessage(),
|
||||||
|
'code' => $e->getCode(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash)
|
||||||
|
{
|
||||||
|
$amount = array_sum(array_column($payment_hash->invoices(), 'amount')) + $payment_hash->fee_total;
|
||||||
|
|
||||||
|
$invoice = Invoice::whereIn('id', $this->transformKeys(array_column($payment_hash->invoices(), 'invoice_id')))->first();
|
||||||
|
|
||||||
|
if ($invoice) {
|
||||||
|
$description = "Invoice {$invoice->number} for {$amount} for client {$this->client->present()->name()}";
|
||||||
|
} else {
|
||||||
|
$description = "Payment with no invoice for amount {$amount} for client {$this->client->present()->name()}";
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->init();
|
||||||
|
|
||||||
|
$result = $this->gateway->transaction()->sale([
|
||||||
|
'amount' => $amount,
|
||||||
|
'paymentMethodToken' => $cgt->token,
|
||||||
|
'deviceData' => '',
|
||||||
|
'options' => [
|
||||||
|
'submitForSettlement' => true
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($result->success) {
|
||||||
|
$this->confirmGatewayFee();
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'payment_type' => PaymentType::parseCardType(strtolower($result->transaction->creditCard['cardType'])),
|
||||||
|
'amount' => $amount,
|
||||||
|
'transaction_reference' => $result->transaction->id,
|
||||||
|
'gateway_type_id' => GatewayType::CREDIT_CARD,
|
||||||
|
];
|
||||||
|
|
||||||
|
$payment = $this->createPayment($data, \App\Models\Payment::STATUS_COMPLETED);
|
||||||
|
|
||||||
|
SystemLogger::dispatch(
|
||||||
|
['response' => $result, 'data' => $data],
|
||||||
|
SystemLog::CATEGORY_GATEWAY_RESPONSE,
|
||||||
|
SystemLog::EVENT_GATEWAY_SUCCESS,
|
||||||
|
SystemLog::TYPE_BRAINTREE,
|
||||||
|
$this->client
|
||||||
|
);
|
||||||
|
|
||||||
|
return $payment;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$result->success) {
|
||||||
|
$this->unWindGatewayFees($payment_hash);
|
||||||
|
|
||||||
|
PaymentFailureMailer::dispatch($this->client, $result->transaction->additionalProcessorResponse, $this->client->company, $this->payment_hash->data->amount_with_fee);
|
||||||
|
|
||||||
|
$message = [
|
||||||
|
'server_response' => $result,
|
||||||
|
'data' => $this->payment_hash->data,
|
||||||
|
];
|
||||||
|
|
||||||
|
SystemLogger::dispatch(
|
||||||
|
$message,
|
||||||
|
SystemLog::CATEGORY_GATEWAY_RESPONSE,
|
||||||
|
SystemLog::EVENT_GATEWAY_FAILURE,
|
||||||
|
SystemLog::TYPE_BRAINTREE,
|
||||||
|
$this->client
|
||||||
|
);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -34,6 +34,7 @@
|
|||||||
"authorizenet/authorizenet": "^2.0",
|
"authorizenet/authorizenet": "^2.0",
|
||||||
"bacon/bacon-qr-code": "^2.0",
|
"bacon/bacon-qr-code": "^2.0",
|
||||||
"beganovich/snappdf": "^1.0",
|
"beganovich/snappdf": "^1.0",
|
||||||
|
"braintree/braintree_php": "^6.0",
|
||||||
"checkout/checkout-sdk-php": "^1.0",
|
"checkout/checkout-sdk-php": "^1.0",
|
||||||
"cleverit/ubl_invoice": "^1.3",
|
"cleverit/ubl_invoice": "^1.3",
|
||||||
"coconutcraig/laravel-postmark": "^2.10",
|
"coconutcraig/laravel-postmark": "^2.10",
|
||||||
|
103
composer.lock
generated
103
composer.lock
generated
@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "38a79899673526624db4d62a76dd9a5e",
|
"content-hash": "cbd0c778d0092866b6c2d3f693f0e5fe",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "asm/php-ansible",
|
"name": "asm/php-ansible",
|
||||||
@ -12,12 +12,12 @@
|
|||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/maschmann/php-ansible.git",
|
"url": "https://github.com/maschmann/php-ansible.git",
|
||||||
"reference": "4f2145cad264fd9f800baf6d3a79dd43fd8009db"
|
"reference": "d526011521ea8f3433d8e940d2a1839474b1c1f4"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/maschmann/php-ansible/zipball/4f2145cad264fd9f800baf6d3a79dd43fd8009db",
|
"url": "https://api.github.com/repos/maschmann/php-ansible/zipball/d526011521ea8f3433d8e940d2a1839474b1c1f4",
|
||||||
"reference": "4f2145cad264fd9f800baf6d3a79dd43fd8009db",
|
"reference": "d526011521ea8f3433d8e940d2a1839474b1c1f4",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@ -54,9 +54,9 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/maschmann/php-ansible/issues",
|
"issues": "https://github.com/maschmann/php-ansible/issues",
|
||||||
"source": "https://github.com/maschmann/php-ansible/tree/master"
|
"source": "https://github.com/maschmann/php-ansible/tree/v2.2"
|
||||||
},
|
},
|
||||||
"time": "2021-03-02T18:27:29+00:00"
|
"time": "2021-05-09T14:23:09+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "authorizenet/authorizenet",
|
"name": "authorizenet/authorizenet",
|
||||||
@ -297,6 +297,55 @@
|
|||||||
},
|
},
|
||||||
"time": "2021-03-19T21:20:07+00:00"
|
"time": "2021-03-19T21:20:07+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "braintree/braintree_php",
|
||||||
|
"version": "6.1.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/braintree/braintree_php.git",
|
||||||
|
"reference": "2406535506ebdbfd685596d890746a4a2db6fa9e"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/braintree/braintree_php/zipball/2406535506ebdbfd685596d890746a4a2db6fa9e",
|
||||||
|
"reference": "2406535506ebdbfd685596d890746a4a2db6fa9e",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-curl": "*",
|
||||||
|
"ext-dom": "*",
|
||||||
|
"ext-hash": "*",
|
||||||
|
"ext-openssl": "*",
|
||||||
|
"ext-xmlwriter": "*",
|
||||||
|
"php": ">=7.3.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^9.0",
|
||||||
|
"squizlabs/php_codesniffer": "^3.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Braintree\\": "lib/Braintree"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Braintree",
|
||||||
|
"homepage": "https://www.braintreepayments.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Braintree PHP Client Library",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/braintree/braintree_php/issues",
|
||||||
|
"source": "https://github.com/braintree/braintree_php/tree/6.1.0"
|
||||||
|
},
|
||||||
|
"time": "2021-05-06T20:43:19+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "brick/math",
|
"name": "brick/math",
|
||||||
"version": "0.9.2",
|
"version": "0.9.2",
|
||||||
@ -2106,16 +2155,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "google/apiclient-services",
|
"name": "google/apiclient-services",
|
||||||
"version": "v0.173.0",
|
"version": "v0.174.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/googleapis/google-api-php-client-services.git",
|
"url": "https://github.com/googleapis/google-api-php-client-services.git",
|
||||||
"reference": "9034b5ba3e25c9ad8e49b6457b9cad21fd9d9847"
|
"reference": "004c5280f5a26a8acbb6f6af6a792e4872b7648a"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/9034b5ba3e25c9ad8e49b6457b9cad21fd9d9847",
|
"url": "https://api.github.com/repos/googleapis/google-api-php-client-services/zipball/004c5280f5a26a8acbb6f6af6a792e4872b7648a",
|
||||||
"reference": "9034b5ba3e25c9ad8e49b6457b9cad21fd9d9847",
|
"reference": "004c5280f5a26a8acbb6f6af6a792e4872b7648a",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@ -2141,9 +2190,9 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/googleapis/google-api-php-client-services/issues",
|
"issues": "https://github.com/googleapis/google-api-php-client-services/issues",
|
||||||
"source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.173.0"
|
"source": "https://github.com/googleapis/google-api-php-client-services/tree/v0.174.0"
|
||||||
},
|
},
|
||||||
"time": "2021-05-02T11:20:02+00:00"
|
"time": "2021-05-08T11:20:03+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "google/auth",
|
"name": "google/auth",
|
||||||
@ -3884,28 +3933,27 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "league/omnipay",
|
"name": "league/omnipay",
|
||||||
"version": "dev-master",
|
"version": "v3.1.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/thephpleague/omnipay.git",
|
"url": "https://github.com/thephpleague/omnipay.git",
|
||||||
"reference": "d090c000030fc759e32f6f747873b5d630103030"
|
"reference": "1ba7c8a3312cf2342458b99c9e5b86eaae44aed2"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/thephpleague/omnipay/zipball/d090c000030fc759e32f6f747873b5d630103030",
|
"url": "https://api.github.com/repos/thephpleague/omnipay/zipball/1ba7c8a3312cf2342458b99c9e5b86eaae44aed2",
|
||||||
"reference": "d090c000030fc759e32f6f747873b5d630103030",
|
"reference": "1ba7c8a3312cf2342458b99c9e5b86eaae44aed2",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"omnipay/common": "^3",
|
"omnipay/common": "^3",
|
||||||
"php": "^7.2|^8.0",
|
"php": "^7.2",
|
||||||
"php-http/discovery": "^1.12",
|
"php-http/discovery": "^1.12",
|
||||||
"php-http/guzzle7-adapter": "^0.1"
|
"php-http/guzzle7-adapter": "^0.1"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"omnipay/tests": "^3"
|
"omnipay/tests": "^3"
|
||||||
},
|
},
|
||||||
"default-branch": true,
|
|
||||||
"type": "metapackage",
|
"type": "metapackage",
|
||||||
"extra": {
|
"extra": {
|
||||||
"branch-alias": {
|
"branch-alias": {
|
||||||
@ -3936,9 +3984,9 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/thephpleague/omnipay/issues",
|
"issues": "https://github.com/thephpleague/omnipay/issues",
|
||||||
"source": "https://github.com/thephpleague/omnipay/tree/master"
|
"source": "https://github.com/thephpleague/omnipay/tree/v3.1.0"
|
||||||
},
|
},
|
||||||
"time": "2021-05-02T15:02:18+00:00"
|
"time": "2020-09-22T14:02:17+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "livewire/livewire",
|
"name": "livewire/livewire",
|
||||||
@ -4689,21 +4737,21 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "omnipay/common",
|
"name": "omnipay/common",
|
||||||
"version": "dev-master",
|
"version": "v3.0.5",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/thephpleague/omnipay-common.git",
|
"url": "https://github.com/thephpleague/omnipay-common.git",
|
||||||
"reference": "e1ebc22615f14219d31cefdf62d7036feb228b1c"
|
"reference": "0d1f4486c1c873537ac030d37c7ce2986c4de1d2"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/thephpleague/omnipay-common/zipball/e1ebc22615f14219d31cefdf62d7036feb228b1c",
|
"url": "https://api.github.com/repos/thephpleague/omnipay-common/zipball/0d1f4486c1c873537ac030d37c7ce2986c4de1d2",
|
||||||
"reference": "e1ebc22615f14219d31cefdf62d7036feb228b1c",
|
"reference": "0d1f4486c1c873537ac030d37c7ce2986c4de1d2",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"moneyphp/money": "^3.1",
|
"moneyphp/money": "^3.1",
|
||||||
"php": "^5.6|^7|^8",
|
"php": "^5.6|^7",
|
||||||
"php-http/client-implementation": "^1",
|
"php-http/client-implementation": "^1",
|
||||||
"php-http/discovery": "^1.2.1",
|
"php-http/discovery": "^1.2.1",
|
||||||
"php-http/message": "^1.5",
|
"php-http/message": "^1.5",
|
||||||
@ -4718,7 +4766,6 @@
|
|||||||
"suggest": {
|
"suggest": {
|
||||||
"league/omnipay": "The default Omnipay package provides a default HTTP Adapter."
|
"league/omnipay": "The default Omnipay package provides a default HTTP Adapter."
|
||||||
},
|
},
|
||||||
"default-branch": true,
|
|
||||||
"type": "library",
|
"type": "library",
|
||||||
"extra": {
|
"extra": {
|
||||||
"branch-alias": {
|
"branch-alias": {
|
||||||
@ -4770,9 +4817,9 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/thephpleague/omnipay-common/issues",
|
"issues": "https://github.com/thephpleague/omnipay-common/issues",
|
||||||
"source": "https://github.com/thephpleague/omnipay-common/tree/master"
|
"source": "https://github.com/thephpleague/omnipay-common/tree/v3.0.5"
|
||||||
},
|
},
|
||||||
"time": "2020-12-13T12:53:48+00:00"
|
"time": "2020-08-20T18:22:12+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "omnipay/paypal",
|
"name": "omnipay/paypal",
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
{
|
{
|
||||||
"video": false,
|
"video": false,
|
||||||
"baseUrl": "https://localhost:8080/",
|
"baseUrl": "http://localhost:8080/",
|
||||||
"chromeWebSecurity": false,
|
"chromeWebSecurity": false,
|
||||||
"env": {
|
"env": {
|
||||||
"runningEnvironment": "docker"
|
"runningEnvironment": "docker"
|
||||||
}
|
},
|
||||||
|
"viewportWidth": 1280,
|
||||||
|
"viewportHeight": 800
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ describe('Credits', () => {
|
|||||||
cy.visit('/client/credits');
|
cy.visit('/client/credits');
|
||||||
|
|
||||||
cy.get('body')
|
cy.get('body')
|
||||||
.find('span')
|
.find('[data-ref=meta-title]')
|
||||||
.first()
|
.first()
|
||||||
.should('contain.text', 'Credits');
|
.should('contain.text', 'Credits');
|
||||||
});
|
});
|
||||||
|
@ -14,7 +14,7 @@ context('Invoices', () => {
|
|||||||
cy.visit('/client/invoices');
|
cy.visit('/client/invoices');
|
||||||
|
|
||||||
cy.get('body')
|
cy.get('body')
|
||||||
.find('span')
|
.find('[data-ref=meta-title]')
|
||||||
.first()
|
.first()
|
||||||
.should('contain.text', 'Invoices');
|
.should('contain.text', 'Invoices');
|
||||||
});
|
});
|
||||||
|
@ -14,7 +14,7 @@ context('Payment methods', () => {
|
|||||||
cy.visit('/client/payment_methods');
|
cy.visit('/client/payment_methods');
|
||||||
|
|
||||||
cy.get('body')
|
cy.get('body')
|
||||||
.find('span')
|
.find('[data-ref=meta-title]')
|
||||||
.first()
|
.first()
|
||||||
.should('contain.text', 'Payment Method');
|
.should('contain.text', 'Payment Method');
|
||||||
});
|
});
|
||||||
|
@ -14,7 +14,7 @@ context('Payments', () => {
|
|||||||
cy.visit('/client/payments');
|
cy.visit('/client/payments');
|
||||||
|
|
||||||
cy.get('body')
|
cy.get('body')
|
||||||
.find('span')
|
.find('[data-ref=meta-title]')
|
||||||
.first()
|
.first()
|
||||||
.should('contain.text', 'Payments');
|
.should('contain.text', 'Payments');
|
||||||
});
|
});
|
||||||
|
@ -14,7 +14,7 @@ describe('Quotes', () => {
|
|||||||
cy.visit('/client/quotes');
|
cy.visit('/client/quotes');
|
||||||
|
|
||||||
cy.get('body')
|
cy.get('body')
|
||||||
.find('span')
|
.find('[data-ref=meta-title]')
|
||||||
.first()
|
.first()
|
||||||
.should('contain.text', 'Quotes');
|
.should('contain.text', 'Quotes');
|
||||||
});
|
});
|
||||||
|
@ -14,7 +14,8 @@ context('Recurring invoices', () => {
|
|||||||
it('should show reucrring invoices text', () => {
|
it('should show reucrring invoices text', () => {
|
||||||
cy.visit('/client/recurring_invoices');
|
cy.visit('/client/recurring_invoices');
|
||||||
|
|
||||||
cy.get('span')
|
cy.get('body')
|
||||||
|
.find('[data-ref=meta-title]')
|
||||||
.first()
|
.first()
|
||||||
.should('contain.text', 'Recurring Invoices');
|
.should('contain.text', 'Recurring Invoices');
|
||||||
});
|
});
|
||||||
|
75
cypress/integration/gateways/braintree_credit_card.spec.js
vendored
Normal file
75
cypress/integration/gateways/braintree_credit_card.spec.js
vendored
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
context('Checkout.com: Credit card testing', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.clientLogin();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cy.visit('/client/logout');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not be able to add payment method', function () {
|
||||||
|
cy.visit('/client/payment_methods');
|
||||||
|
|
||||||
|
cy.get('[data-cy=add-payment-method]').click();
|
||||||
|
cy.get('[data-cy=add-credit-card-link]').click();
|
||||||
|
|
||||||
|
cy.get('[data-ref=gateway-container]')
|
||||||
|
.contains('This payment method can be can saved for future use, once you complete your first transaction. Don\'t forget to check "Store credit card details" during payment process.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pay with new card', function () {
|
||||||
|
cy.visit('/client/invoices');
|
||||||
|
|
||||||
|
cy.get('[data-cy=pay-now]').first().click();
|
||||||
|
cy.get('[data-cy=pay-now-dropdown]').click();
|
||||||
|
cy.get('[data-cy=pay-with-0]').click();
|
||||||
|
|
||||||
|
cy
|
||||||
|
.get('#braintree-hosted-field-number')
|
||||||
|
.wait(5000)
|
||||||
|
.iframeLoaded()
|
||||||
|
.its('document')
|
||||||
|
.getInDocument('#credit-card-number')
|
||||||
|
.type(4111111111111111)
|
||||||
|
|
||||||
|
cy
|
||||||
|
.get('#braintree-hosted-field-expirationDate')
|
||||||
|
.wait(5000)
|
||||||
|
.iframeLoaded()
|
||||||
|
.its('document')
|
||||||
|
.getInDocument('#expiration')
|
||||||
|
.type(1224)
|
||||||
|
|
||||||
|
cy.get('#pay-now').click();
|
||||||
|
|
||||||
|
cy.url().should('contain', '/client/payments/VolejRejNm');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pay with saved card (token)', function () {
|
||||||
|
cy.visit('/client/invoices');
|
||||||
|
|
||||||
|
cy.get('[data-cy=pay-now]').first().click();
|
||||||
|
cy.get('[data-cy=pay-now-dropdown]').click();
|
||||||
|
cy.get('[data-cy=pay-with-0]').click();
|
||||||
|
|
||||||
|
cy.get('[name=payment-type]').first().check();
|
||||||
|
|
||||||
|
cy.get('#pay-now-with-token').click();
|
||||||
|
|
||||||
|
cy.url().should('contain', '/client/payments/Opnel5aKBz');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be able to remove payment method', function () {
|
||||||
|
cy.visit('/client/payment_methods');
|
||||||
|
|
||||||
|
cy.get('[data-cy=view-payment-method]').click();
|
||||||
|
|
||||||
|
cy.get('#open-delete-popup').click();
|
||||||
|
|
||||||
|
cy.get('[data-cy=confirm-payment-removal]').click();
|
||||||
|
|
||||||
|
cy.url().should('contain', '/client/payment_methods');
|
||||||
|
|
||||||
|
cy.get('body').contains('Payment method has been successfully removed.');
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Gateway;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class MakeBraintreeProviderVisible extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Gateway::where('id', 50)->update(['visible' => 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
@ -95,7 +95,7 @@ class PaymentLibrariesSeeder extends Seeder
|
|||||||
|
|
||||||
Gateway::query()->update(['visible' => 0]);
|
Gateway::query()->update(['visible' => 0]);
|
||||||
|
|
||||||
Gateway::whereIn('id', [1,15,20,39,55])->update(['visible' => 1]);
|
Gateway::whereIn('id', [1,15,20,39,55,50])->update(['visible' => 1]);
|
||||||
|
|
||||||
Gateway::all()->each(function ($gateway) {
|
Gateway::all()->each(function ($gateway) {
|
||||||
$gateway->site_url = $gateway->getHelp();
|
$gateway->site_url = $gateway->getHelp();
|
||||||
|
2
public/js/clients/payments/braintree-credit-card.js
vendored
Normal file
2
public/js/clients/payments/braintree-credit-card.js
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/*! For license information please see braintree-credit-card.js.LICENSE.txt */
|
||||||
|
!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/",n(n.s=17)}({17:function(e,t,n){e.exports=n("jPAV")},jPAV:function(e,t){function n(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}(new(function(){function e(){!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e)}var t,r,o;return t=e,(r=[{key:"initBraintreeDataCollector",value:function(){window.braintree.client.create({authorization:document.querySelector("meta[name=client-token]").content},(function(e,t){window.braintree.dataCollector.create({client:t,paypal:!0},(function(e,t){e||(document.querySelector("input[name=client-data]").value=t.deviceData)}))}))}},{key:"mountBraintreePaymentWidget",value:function(){window.braintree.dropin.create({authorization:document.querySelector("meta[name=client-token]").content,container:"#dropin-container"},this.handleCallback)}},{key:"handleCallback",value:function(e,t){if(e)console.error(e);else{var n=document.getElementById("pay-now");n.addEventListener("click",(function(){t.requestPaymentMethod((function(e,t){if(e)return console.error(e);n.disabled=!0,n.querySelector("svg").classList.remove("hidden"),n.querySelector("span").classList.add("hidden"),document.querySelector("input[name=gateway_response]").value=JSON.stringify(t);var r=document.querySelector('input[name="token-billing-checkbox"]:checked');r&&(document.querySelector('input[name="store_card"]').value=r.value),document.getElementById("server-response").submit()}))}))}}},{key:"handle",value:function(){this.initBraintreeDataCollector(),this.mountBraintreePaymentWidget(),Array.from(document.getElementsByClassName("toggle-payment-with-token")).forEach((function(e){return e.addEventListener("click",(function(e){document.getElementById("dropin-container").classList.add("hidden"),document.getElementById("save-card--container").style.display="none",document.querySelector("input[name=token]").value=e.target.dataset.token,document.getElementById("pay-now-with-token").classList.remove("hidden"),document.getElementById("pay-now").classList.add("hidden")}))})),document.getElementById("toggle-payment-with-credit-card").addEventListener("click",(function(e){document.getElementById("dropin-container").classList.remove("hidden"),document.getElementById("save-card--container").style.display="grid",document.querySelector("input[name=token]").value="",document.getElementById("pay-now-with-token").classList.add("hidden"),document.getElementById("pay-now").classList.remove("hidden")}));var e=document.getElementById("pay-now-with-token");e.addEventListener("click",(function(t){e.disabled=!0,e.querySelector("svg").classList.remove("hidden"),e.querySelector("span").classList.add("hidden"),document.getElementById("server-response").submit()}))}}])&&n(t.prototype,r),o&&n(t,o),e}())).handle()}});
|
@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://opensource.org/licenses/AAL
|
||||||
|
*/
|
2
public/js/clients/payments/braintree-paypal.js
vendored
Normal file
2
public/js/clients/payments/braintree-paypal.js
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/*! For license information please see braintree-paypal.js.LICENSE.txt */
|
||||||
|
!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/",n(n.s=18)}({18:function(e,t,n){e.exports=n("cZZG")},cZZG:function(e,t){function n(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}(new(function(){function e(){!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e)}var t,r,o;return t=e,o=[{key:"getPaymentDetails",value:function(){return{flow:"vault"}}},{key:"handleErrorMessage",value:function(e){var t=document.getElementById("errors");t.innerText=e,t.hidden=!1}}],(r=[{key:"initBraintreeDataCollector",value:function(){window.braintree.client.create({authorization:document.querySelector("meta[name=client-token]").content},(function(e,t){window.braintree.dataCollector.create({client:t,paypal:!0},(function(e,t){e||(document.querySelector("input[name=client-data]").value=t.deviceData)}))}))}},{key:"handlePaymentWithToken",value:function(){Array.from(document.getElementsByClassName("toggle-payment-with-token")).forEach((function(e){return e.addEventListener("click",(function(e){document.getElementById("paypal-button").classList.add("hidden"),document.getElementById("save-card--container").style.display="none",document.querySelector("input[name=token]").value=e.target.dataset.token,document.getElementById("pay-now-with-token").classList.remove("hidden"),document.getElementById("pay-now").classList.add("hidden")}))}));var e=document.getElementById("pay-now-with-token");e.addEventListener("click",(function(t){e.disabled=!0,e.querySelector("svg").classList.remove("hidden"),e.querySelector("span").classList.add("hidden"),document.getElementById("server-response").submit()}))}},{key:"handle",value:function(){this.initBraintreeDataCollector(),this.handlePaymentWithToken(),braintree.client.create({authorization:document.querySelector("meta[name=client-token]").content}).then((function(e){return braintree.paypalCheckout.create({client:e})})).then((function(t){return t.loadPayPalSDK({vault:!0}).then((function(t){return paypal.Buttons({fundingSource:paypal.FUNDING.PAYPAL,createBillingAgreement:function(){return t.createPayment(e.getPaymentDetails())},onApprove:function(e,n){return t.tokenizePayment(e).then((function(e){var t=document.querySelector('input[name="token-billing-checkbox"]:checked');t&&(document.querySelector('input[name="store_card"]').value=t.value),document.querySelector("input[name=gateway_response]").value=JSON.stringify(e),document.getElementById("server-response").submit()}))},onCancel:function(e){},onError:function(t){console.log(t.message),e.handleErrorMessage(t.message)}}).render("#paypal-button")}))})).catch((function(t){console.log(t.message),e.handleErrorMessage(t.message)}))}}])&&n(t.prototype,r),o&&n(t,o),e}())).handle()}});
|
@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://opensource.org/licenses/AAL
|
||||||
|
*/
|
@ -6,6 +6,8 @@
|
|||||||
"/js/clients/linkify-urls.js": "/js/clients/linkify-urls.js?id=0dc8c34010d09195d2f7",
|
"/js/clients/linkify-urls.js": "/js/clients/linkify-urls.js?id=0dc8c34010d09195d2f7",
|
||||||
"/js/clients/payment_methods/authorize-authorize-card.js": "/js/clients/payment_methods/authorize-authorize-card.js?id=206d7de4ac97612980ff",
|
"/js/clients/payment_methods/authorize-authorize-card.js": "/js/clients/payment_methods/authorize-authorize-card.js?id=206d7de4ac97612980ff",
|
||||||
"/js/clients/payments/authorize-credit-card-payment.js": "/js/clients/payments/authorize-credit-card-payment.js?id=a376eff2227da398b0ba",
|
"/js/clients/payments/authorize-credit-card-payment.js": "/js/clients/payments/authorize-credit-card-payment.js?id=a376eff2227da398b0ba",
|
||||||
|
"/js/clients/payments/braintree-credit-card.js": "/js/clients/payments/braintree-credit-card.js?id=81957e7cb1cb49f23b90",
|
||||||
|
"/js/clients/payments/braintree-paypal.js": "/js/clients/payments/braintree-paypal.js?id=c35db3cbb65806ab6a8a",
|
||||||
"/js/clients/payments/card-js.min.js": "/js/clients/payments/card-js.min.js?id=5469146cd629ea1b5c20",
|
"/js/clients/payments/card-js.min.js": "/js/clients/payments/card-js.min.js?id=5469146cd629ea1b5c20",
|
||||||
"/js/clients/payments/checkout-credit-card.js": "/js/clients/payments/checkout-credit-card.js?id=98e406fa8e4db0e93427",
|
"/js/clients/payments/checkout-credit-card.js": "/js/clients/payments/checkout-credit-card.js?id=98e406fa8e4db0e93427",
|
||||||
"/js/clients/payments/stripe-ach.js": "/js/clients/payments/stripe-ach.js?id=76d8ba6a814b3015e359",
|
"/js/clients/payments/stripe-ach.js": "/js/clients/payments/stripe-ach.js?id=76d8ba6a814b3015e359",
|
||||||
|
112
resources/js/clients/payments/braintree-credit-card.js
vendored
Normal file
112
resources/js/clients/payments/braintree-credit-card.js
vendored
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://opensource.org/licenses/AAL
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
class BraintreeCreditCard {
|
||||||
|
initBraintreeDataCollector() {
|
||||||
|
window.braintree.client.create({
|
||||||
|
authorization: document.querySelector('meta[name=client-token]').content
|
||||||
|
}, function (err, clientInstance) {
|
||||||
|
window.braintree.dataCollector.create({
|
||||||
|
client: clientInstance,
|
||||||
|
paypal: true
|
||||||
|
}, function (err, dataCollectorInstance) {
|
||||||
|
if (err) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelector('input[name=client-data]').value = dataCollectorInstance.deviceData;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
mountBraintreePaymentWidget() {
|
||||||
|
window.braintree.dropin.create({
|
||||||
|
authorization: document.querySelector('meta[name=client-token]').content,
|
||||||
|
container: '#dropin-container'
|
||||||
|
}, this.handleCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCallback(error, dropinInstance) {
|
||||||
|
if (error) {
|
||||||
|
console.error(error);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let payNow = document.getElementById('pay-now');
|
||||||
|
|
||||||
|
payNow.addEventListener('click', () => {
|
||||||
|
dropinInstance.requestPaymentMethod((error, payload) => {
|
||||||
|
if (error) {
|
||||||
|
return console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
payNow.disabled = true;
|
||||||
|
|
||||||
|
payNow.querySelector('svg').classList.remove('hidden');
|
||||||
|
payNow.querySelector('span').classList.add('hidden');
|
||||||
|
|
||||||
|
document.querySelector('input[name=gateway_response]').value = JSON.stringify(payload);
|
||||||
|
|
||||||
|
let tokenBillingCheckbox = document.querySelector(
|
||||||
|
'input[name="token-billing-checkbox"]:checked'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (tokenBillingCheckbox) {
|
||||||
|
document.querySelector('input[name="store_card"]').value =
|
||||||
|
tokenBillingCheckbox.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('server-response').submit();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handle() {
|
||||||
|
this.initBraintreeDataCollector();
|
||||||
|
this.mountBraintreePaymentWidget();
|
||||||
|
|
||||||
|
Array
|
||||||
|
.from(document.getElementsByClassName('toggle-payment-with-token'))
|
||||||
|
.forEach((element) => element.addEventListener('click', (element) => {
|
||||||
|
document.getElementById('dropin-container').classList.add('hidden');
|
||||||
|
document.getElementById('save-card--container').style.display = 'none';
|
||||||
|
document.querySelector('input[name=token]').value = element.target.dataset.token;
|
||||||
|
|
||||||
|
document.getElementById('pay-now-with-token').classList.remove('hidden');
|
||||||
|
document.getElementById('pay-now').classList.add('hidden');
|
||||||
|
}));
|
||||||
|
|
||||||
|
document
|
||||||
|
.getElementById('toggle-payment-with-credit-card')
|
||||||
|
.addEventListener('click', (element) => {
|
||||||
|
document.getElementById('dropin-container').classList.remove('hidden');
|
||||||
|
document.getElementById('save-card--container').style.display = 'grid';
|
||||||
|
document.querySelector('input[name=token]').value = "";
|
||||||
|
|
||||||
|
document.getElementById('pay-now-with-token').classList.add('hidden');
|
||||||
|
document.getElementById('pay-now').classList.remove('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
let payNowWithToken = document.getElementById('pay-now-with-token');
|
||||||
|
|
||||||
|
payNowWithToken
|
||||||
|
.addEventListener('click', (element) => {
|
||||||
|
payNowWithToken.disabled = true;
|
||||||
|
payNowWithToken.querySelector('svg').classList.remove('hidden');
|
||||||
|
payNowWithToken.querySelector('span').classList.add('hidden');
|
||||||
|
|
||||||
|
document.getElementById('server-response').submit();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new BraintreeCreditCard().handle();
|
122
resources/js/clients/payments/braintree-paypal.js
vendored
Normal file
122
resources/js/clients/payments/braintree-paypal.js
vendored
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com).
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://opensource.org/licenses/AAL
|
||||||
|
*/
|
||||||
|
|
||||||
|
class BraintreePayPal {
|
||||||
|
initBraintreeDataCollector() {
|
||||||
|
window.braintree.client.create({
|
||||||
|
authorization: document.querySelector('meta[name=client-token]').content
|
||||||
|
}, function (err, clientInstance) {
|
||||||
|
window.braintree.dataCollector.create({
|
||||||
|
client: clientInstance,
|
||||||
|
paypal: true
|
||||||
|
}, function (err, dataCollectorInstance) {
|
||||||
|
if (err) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelector('input[name=client-data]').value = dataCollectorInstance.deviceData;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static getPaymentDetails() {
|
||||||
|
return {
|
||||||
|
flow: 'vault',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static handleErrorMessage(message) {
|
||||||
|
let errorsContainer = document.getElementById('errors');
|
||||||
|
|
||||||
|
errorsContainer.innerText = message;
|
||||||
|
errorsContainer.hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePaymentWithToken() {
|
||||||
|
Array
|
||||||
|
.from(document.getElementsByClassName('toggle-payment-with-token'))
|
||||||
|
.forEach((element) => element.addEventListener('click', (element) => {
|
||||||
|
document.getElementById('paypal-button').classList.add('hidden');
|
||||||
|
document.getElementById('save-card--container').style.display = 'none';
|
||||||
|
document.querySelector('input[name=token]').value = element.target.dataset.token;
|
||||||
|
|
||||||
|
document.getElementById('pay-now-with-token').classList.remove('hidden');
|
||||||
|
document.getElementById('pay-now').classList.add('hidden');
|
||||||
|
}));
|
||||||
|
|
||||||
|
let payNowWithToken = document.getElementById('pay-now-with-token');
|
||||||
|
|
||||||
|
payNowWithToken
|
||||||
|
.addEventListener('click', (element) => {
|
||||||
|
payNowWithToken.disabled = true;
|
||||||
|
payNowWithToken.querySelector('svg').classList.remove('hidden');
|
||||||
|
payNowWithToken.querySelector('span').classList.add('hidden');
|
||||||
|
|
||||||
|
document.getElementById('server-response').submit();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handle() {
|
||||||
|
this.initBraintreeDataCollector();
|
||||||
|
this.handlePaymentWithToken();
|
||||||
|
|
||||||
|
braintree.client.create({
|
||||||
|
authorization: document.querySelector('meta[name=client-token]').content,
|
||||||
|
}).then(function (clientInstance) {
|
||||||
|
return braintree.paypalCheckout.create({
|
||||||
|
client: clientInstance
|
||||||
|
});
|
||||||
|
}).then(function (paypalCheckoutInstance) {
|
||||||
|
return paypalCheckoutInstance.loadPayPalSDK({
|
||||||
|
vault: true
|
||||||
|
}).then(function (paypalCheckoutInstance) {
|
||||||
|
return paypal.Buttons({
|
||||||
|
fundingSource: paypal.FUNDING.PAYPAL,
|
||||||
|
|
||||||
|
createBillingAgreement: function () {
|
||||||
|
return paypalCheckoutInstance.createPayment(BraintreePayPal.getPaymentDetails());
|
||||||
|
},
|
||||||
|
|
||||||
|
onApprove: function (data, actions) {
|
||||||
|
return paypalCheckoutInstance.tokenizePayment(data).then(function (payload) {
|
||||||
|
let tokenBillingCheckbox = document.querySelector(
|
||||||
|
'input[name="token-billing-checkbox"]:checked'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (tokenBillingCheckbox) {
|
||||||
|
document.querySelector('input[name="store_card"]').value =
|
||||||
|
tokenBillingCheckbox.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelector('input[name=gateway_response]').value = JSON.stringify(payload);
|
||||||
|
document.getElementById('server-response').submit();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
onCancel: function (data) {
|
||||||
|
// ..
|
||||||
|
},
|
||||||
|
|
||||||
|
onError: function (err) {
|
||||||
|
console.log(err.message);
|
||||||
|
|
||||||
|
BraintreePayPal.handleErrorMessage(err.message);
|
||||||
|
}
|
||||||
|
}).render('#paypal-button');
|
||||||
|
});
|
||||||
|
}).catch(function (err) {
|
||||||
|
console.log(err.message);
|
||||||
|
|
||||||
|
BraintreePayPal.handleErrorMessage(err.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new BraintreePayPal().handle();
|
@ -4239,7 +4239,8 @@ $LANG = array(
|
|||||||
'max_companies_desc' => 'You have reached your maximum number of companies. Delete existing companies to migrate new ones.',
|
'max_companies_desc' => 'You have reached your maximum number of companies. Delete existing companies to migrate new ones.',
|
||||||
'migration_already_completed' => 'Company already migrated',
|
'migration_already_completed' => 'Company already migrated',
|
||||||
'migration_already_completed_desc' => 'Looks like you already migrated <b> :company_name </b>to the V5 version of the Invoice Ninja. In case you want to start over, you can force migrate to wipe existing data.',
|
'migration_already_completed_desc' => 'Looks like you already migrated <b> :company_name </b>to the V5 version of the Invoice Ninja. In case you want to start over, you can force migrate to wipe existing data.',
|
||||||
|
'payment_method_cannot_be_authorized_first' => 'This payment method can be can saved for future use, once you complete your first transaction. Don\'t forget to check "Store credit card details" during payment process.',
|
||||||
|
'new_account' => 'New account',
|
||||||
'activity_100' => ':user created recurring invoice :recurring_invoice',
|
'activity_100' => ':user created recurring invoice :recurring_invoice',
|
||||||
'activity_101' => ':user updated recurring invoice :recurring_invoice',
|
'activity_101' => ':user updated recurring invoice :recurring_invoice',
|
||||||
'activity_102' => ':user archived recurring invoice :recurring_invoice',
|
'activity_102' => ':user archived recurring invoice :recurring_invoice',
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<div class="flex-1 px-3 md:px-8 flex justify-between items-center">
|
<div class="flex-1 px-3 md:px-8 flex justify-between items-center">
|
||||||
<span class="text-xl text-gray-900">@yield('meta_title')</span>
|
<span class="text-xl text-gray-900" data-ref="meta-title">@yield('meta_title')</span>
|
||||||
<div class="flex items-center md:ml-6 md:mr-2">
|
<div class="flex items-center md:ml-6 md:mr-2">
|
||||||
@if($multiple_contacts->count() > 1)
|
@if($multiple_contacts->count() > 1)
|
||||||
<div class="relative inline-block text-left" x-data="{ open: false }">
|
<div class="relative inline-block text-left" x-data="{ open: false }">
|
||||||
|
@ -26,6 +26,7 @@
|
|||||||
<div class="mx-auto px-4 sm:px-6 md:px-8">
|
<div class="mx-auto px-4 sm:px-6 md:px-8">
|
||||||
<div class="pt-4 py-6">
|
<div class="pt-4 py-6">
|
||||||
@includeWhen(session()->has('success'), 'portal.ninja2020.components.general.messages.success')
|
@includeWhen(session()->has('success'), 'portal.ninja2020.components.general.messages.success')
|
||||||
|
|
||||||
{{ $slot }}
|
{{ $slot }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
@extends('portal.ninja2020.layout.payments', ['gateway_title' => 'Credit card', 'card_title' => 'Credit card'])
|
||||||
|
|
||||||
|
@section('gateway_content')
|
||||||
|
@component('portal.ninja2020.components.general.card-element-single', ['title' => 'Credit card', 'show_title' => false])
|
||||||
|
{{ __('texts.payment_method_cannot_be_authorized_first') }}
|
||||||
|
@endcomponent
|
||||||
|
@endsection
|
@ -0,0 +1,74 @@
|
|||||||
|
@extends('portal.ninja2020.layout.payments', ['gateway_title' => ctrans('texts.credit_card'), 'card_title' => ctrans('texts.credit_card')])
|
||||||
|
|
||||||
|
@section('gateway_head')
|
||||||
|
<meta name="client-token" content="{{ $client_token ?? '' }}"/>
|
||||||
|
|
||||||
|
<script src="https://js.braintreegateway.com/web/dropin/1.27.0/js/dropin.min.js"></script>
|
||||||
|
<script src="https://js.braintreegateway.com/web/3.76.2/js/client.min.js"></script>
|
||||||
|
<script src="https://js.braintreegateway.com/web/3.76.2/js/data-collector.min.js"></script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
[data-braintree-id="toggle"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('gateway_content')
|
||||||
|
<form action="{{ route('client.payments.response') }}" method="post" id="server-response">
|
||||||
|
@csrf
|
||||||
|
<input type="hidden" name="gateway_response">
|
||||||
|
<input type="hidden" name="store_card">
|
||||||
|
<input type="hidden" name="payment_hash" value="{{ $payment_hash }}">
|
||||||
|
|
||||||
|
<input type="hidden" name="company_gateway_id" value="{{ $gateway->getCompanyGatewayId() }}">
|
||||||
|
<input type="hidden" name="payment_method_id" value="{{ $payment_method_id }}">
|
||||||
|
|
||||||
|
<input type="hidden" name="token">
|
||||||
|
<input type="hidden" name="client-data">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.payment_type')])
|
||||||
|
{{ ctrans('texts.credit_card') }}
|
||||||
|
@endcomponent
|
||||||
|
|
||||||
|
@include('portal.ninja2020.gateways.includes.payment_details')
|
||||||
|
|
||||||
|
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.pay_with')])
|
||||||
|
@if(count($tokens) > 0)
|
||||||
|
@foreach($tokens as $token)
|
||||||
|
<label class="mr-4">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
data-token="{{ $token->token }}"
|
||||||
|
name="payment-type"
|
||||||
|
class="form-radio cursor-pointer toggle-payment-with-token"/>
|
||||||
|
<span class="ml-1 cursor-pointer">**** {{ optional($token->meta)->last4 }}</span>
|
||||||
|
</label>
|
||||||
|
@endforeach
|
||||||
|
@endisset
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id="toggle-payment-with-credit-card"
|
||||||
|
class="form-radio cursor-pointer"
|
||||||
|
name="payment-type"
|
||||||
|
checked/>
|
||||||
|
<span class="ml-1 cursor-pointer">{{ __('texts.new_card') }}</span>
|
||||||
|
</label>
|
||||||
|
@endcomponent
|
||||||
|
|
||||||
|
@include('portal.ninja2020.gateways.includes.save_card')
|
||||||
|
|
||||||
|
@component('portal.ninja2020.components.general.card-element-single')
|
||||||
|
<div id="dropin-container"></div>
|
||||||
|
@endcomponent
|
||||||
|
|
||||||
|
@include('portal.ninja2020.gateways.includes.pay_now')
|
||||||
|
@include('portal.ninja2020.gateways.includes.pay_now', ['id' => 'pay-now-with-token', 'class' => 'hidden'])
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('gateway_footer')
|
||||||
|
<script src="{{ asset('js/clients/payments/braintree-credit-card.js') }}"></script>
|
||||||
|
@endsection
|
@ -0,0 +1,7 @@
|
|||||||
|
@extends('portal.ninja2020.layout.payments', ['gateway_title' => ctrans('texts.paypal'), 'card_title' => ctrans('texts.paypal')])
|
||||||
|
|
||||||
|
@section('gateway_content')
|
||||||
|
@component('portal.ninja2020.components.general.card-element-single', ['title' => ctrans('texts.paypal'), 'show_title' => false])
|
||||||
|
{{ __('texts.payment_method_cannot_be_authorized_first') }}
|
||||||
|
@endcomponent
|
||||||
|
@endsection
|
@ -0,0 +1,69 @@
|
|||||||
|
@extends('portal.ninja2020.layout.payments', ['gateway_title' => ctrans('texts.paypal'), 'card_title' => ctrans('texts.paypal')])
|
||||||
|
|
||||||
|
@section('gateway_head')
|
||||||
|
<meta name="client-token" content="{{ $client_token ?? '' }}"/>
|
||||||
|
|
||||||
|
<script src="https://js.braintreegateway.com/web/3.76.2/js/client.min.js"></script>
|
||||||
|
<script src="https://js.braintreegateway.com/web/3.76.2/js/paypal-checkout.min.js"></script>
|
||||||
|
<script src="https://js.braintreegateway.com/web/3.76.2/js/data-collector.min.js"></script>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('gateway_content')
|
||||||
|
<form action="{{ route('client.payments.response') }}" method="post" id="server-response">
|
||||||
|
@csrf
|
||||||
|
<input type="hidden" name="gateway_response">
|
||||||
|
<input type="hidden" name="store_card">
|
||||||
|
<input type="hidden" name="payment_hash" value="{{ $payment_hash }}">
|
||||||
|
|
||||||
|
<input type="hidden" name="company_gateway_id" value="{{ $gateway->getCompanyGatewayId() }}">
|
||||||
|
<input type="hidden" name="payment_method_id" value="{{ $payment_method_id }}">
|
||||||
|
|
||||||
|
<input type="hidden" name="token">
|
||||||
|
<input type="hidden" name="client-data">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="alert alert-failure mb-4" hidden id="errors"></div>
|
||||||
|
|
||||||
|
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.payment_type')])
|
||||||
|
{{ ctrans('texts.paypal') }}
|
||||||
|
@endcomponent
|
||||||
|
|
||||||
|
@include('portal.ninja2020.gateways.includes.payment_details')
|
||||||
|
|
||||||
|
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.pay_with')])
|
||||||
|
@if(count($tokens) > 0)
|
||||||
|
@foreach($tokens as $token)
|
||||||
|
<label class="mr-4 block mt-2">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
data-token="{{ $token->token }}"
|
||||||
|
name="payment-type"
|
||||||
|
class="form-radio cursor-pointer toggle-payment-with-token"/>
|
||||||
|
<span class="ml-1 cursor-pointer">{{ $token->meta->email }}</span>
|
||||||
|
</label>
|
||||||
|
@endforeach
|
||||||
|
@endisset
|
||||||
|
|
||||||
|
<label class="block mt-2">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id="toggle-payment-with-credit-card"
|
||||||
|
class="form-radio cursor-pointer"
|
||||||
|
name="payment-type"
|
||||||
|
checked/>
|
||||||
|
<span class="ml-1 cursor-pointer">{{ __('texts.new_account') }}</span>
|
||||||
|
</label>
|
||||||
|
@endcomponent
|
||||||
|
|
||||||
|
@include('portal.ninja2020.gateways.includes.save_card')
|
||||||
|
|
||||||
|
@component('portal.ninja2020.components.general.card-element-single')
|
||||||
|
<div id="paypal-button"></div>
|
||||||
|
@endcomponent
|
||||||
|
|
||||||
|
@include('portal.ninja2020.gateways.includes.pay_now', ['id' => 'pay-now-with-token', 'class' => 'hidden'])
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@section('gateway_footer')
|
||||||
|
<script src="{{ asset('js/clients/payments/braintree-paypal.js') }}"></script>
|
||||||
|
@endsection
|
@ -4,4 +4,4 @@
|
|||||||
@component('portal.ninja2020.components.general.card-element-single', ['title' => 'Credit card', 'show_title' => false])
|
@component('portal.ninja2020.components.general.card-element-single', ['title' => 'Credit card', 'show_title' => false])
|
||||||
{{ __('texts.checkout_authorize_label') }}
|
{{ __('texts.checkout_authorize_label') }}
|
||||||
@endcomponent
|
@endcomponent
|
||||||
@endsection
|
@endsection
|
||||||
|
@ -16,12 +16,12 @@
|
|||||||
value="true"/>
|
value="true"/>
|
||||||
<span class="ml-1 cursor-pointer">{{ ctrans('texts.yes') }}</span>
|
<span class="ml-1 cursor-pointer">{{ ctrans('texts.yes') }}</span>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<labecoml>
|
||||||
<input type="radio" class="form-radio cursor-pointer" name="token-billing-checkbox"
|
<input type="radio" class="form-radio cursor-pointer" name="token-billing-checkbox"
|
||||||
id="proxy_is_default"
|
id="proxy_is_default"
|
||||||
value="false" checked />
|
value="false" checked />
|
||||||
<span class="ml-1 cursor-pointer">{{ ctrans('texts.no') }}</span>
|
<span class="ml-1 cursor-pointer">{{ ctrans('texts.no') }}</span>
|
||||||
</label>
|
</labecoml>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
|
7
webpack.mix.js
vendored
7
webpack.mix.js
vendored
@ -65,6 +65,13 @@ mix.js("resources/js/app.js", "public/js")
|
|||||||
.js(
|
.js(
|
||||||
"resources/js/clients/linkify-urls.js",
|
"resources/js/clients/linkify-urls.js",
|
||||||
"public/js/clients/linkify-urls.js"
|
"public/js/clients/linkify-urls.js"
|
||||||
|
)
|
||||||
|
.js(
|
||||||
|
"resources/js/clients/payments/braintree-credit-card.js",
|
||||||
|
"public/js/clients/payments/braintree-credit-card.js"
|
||||||
|
).js(
|
||||||
|
"resources/js/clients/payments/braintree-paypal.js",
|
||||||
|
"public/js/clients/payments/braintree-paypal.js"
|
||||||
);
|
);
|
||||||
|
|
||||||
mix.copyDirectory('node_modules/card-js/card-js.min.css', 'public/css/card-js.min.css');
|
mix.copyDirectory('node_modules/card-js/card-js.min.css', 'public/css/card-js.min.css');
|
||||||
|
Loading…
x
Reference in New Issue
Block a user