mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-07-09 03:14:30 -04:00
Adjustments for PayPal Rest implementation
This commit is contained in:
parent
d05a58cebe
commit
9f5cfd7440
@ -519,9 +519,6 @@ class PayPalPPCPPaymentDriver extends BaseDriver
|
||||
->withHeaders($this->getHeaders($headers))
|
||||
->{$verb}("{$this->api_endpoint_url}{$uri}", $data);
|
||||
|
||||
// nlog($r);
|
||||
// nlog($r->json());
|
||||
|
||||
if($r->successful()) {
|
||||
return $r;
|
||||
}
|
||||
|
@ -12,15 +12,16 @@
|
||||
|
||||
namespace App\PaymentDrivers;
|
||||
|
||||
use App\Exceptions\PaymentFailed;
|
||||
use App\Jobs\Util\SystemLogger;
|
||||
use App\Models\GatewayType;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\PaymentType;
|
||||
use App\Models\SystemLog;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Carbon\Carbon;
|
||||
use Omnipay\Omnipay;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\SystemLog;
|
||||
use App\Models\GatewayType;
|
||||
use App\Models\PaymentType;
|
||||
use App\Jobs\Util\SystemLogger;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use App\Exceptions\PaymentFailed;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class PayPalRestPaymentDriver extends BaseDriver
|
||||
{
|
||||
@ -40,6 +41,12 @@ class PayPalRestPaymentDriver extends BaseDriver
|
||||
|
||||
private string $paypal_payment_method = '';
|
||||
|
||||
private ?int $gateway_type_id = null;
|
||||
|
||||
protected mixed $access_token = null;
|
||||
|
||||
protected ?Carbon $token_expiry = null;
|
||||
|
||||
private array $funding_options = [
|
||||
3 => 'paypal',
|
||||
1 => 'card',
|
||||
@ -51,7 +58,7 @@ class PayPalRestPaymentDriver extends BaseDriver
|
||||
// 13 => 'ideal',
|
||||
// 26 => 'mercadopago',
|
||||
// 27 => 'mybank',
|
||||
// 28 => 'paylater',
|
||||
28 => 'paylater',
|
||||
// 16 => 'p24',
|
||||
// 7 => 'sofort'
|
||||
];
|
||||
@ -74,25 +81,61 @@ class PayPalRestPaymentDriver extends BaseDriver
|
||||
|
||||
public function init()
|
||||
{
|
||||
$this->omnipay_gateway = Omnipay::create(
|
||||
$this->company_gateway->gateway->provider
|
||||
);
|
||||
|
||||
$this->omnipay_gateway->initialize((array) $this->company_gateway->getConfig());
|
||||
// $this->omnipay_gateway = Omnipay::create(
|
||||
// $this->company_gateway->gateway->provider
|
||||
// );
|
||||
|
||||
// $this->omnipay_gateway->initialize((array) $this->company_gateway->getConfig());
|
||||
$this->api_endpoint_url = $this->company_gateway->getConfigField('testMode') ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com';
|
||||
|
||||
$secret = $this->company_gateway->getConfigField('secret');
|
||||
$client_id = $this->company_gateway->getConfigField('clientId');
|
||||
|
||||
if($this->access_token && $this->token_expiry && $this->token_expiry->isFuture()) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$response = Http::withBasicAuth($client_id, $secret)
|
||||
->withHeaders(['Content-Type' => 'application/x-www-form-urlencoded'])
|
||||
->withQueryParameters(['grant_type' => 'client_credentials'])
|
||||
->post("{$this->api_endpoint_url}/v1/oauth2/token");
|
||||
|
||||
if($response->successful()) {
|
||||
$this->access_token = $response->json()['access_token'];
|
||||
$this->token_expiry = now()->addSeconds($response->json()['expires_in'] - 60);
|
||||
} else {
|
||||
throw new PaymentFailed('Unable to gain access token from Paypal. Check your configuration', 401);
|
||||
}
|
||||
|
||||
return $this;
|
||||
|
||||
}
|
||||
|
||||
public function setPaymentMethod($payment_method_id)
|
||||
private function getPaymentMethod($gateway_type_id): int
|
||||
{
|
||||
$method = PaymentType::PAYPAL;
|
||||
|
||||
match($gateway_type_id) {
|
||||
"1" => $method = PaymentType::CREDIT_CARD_OTHER,
|
||||
"3" => $method = PaymentType::PAYPAL,
|
||||
"25" => $method = PaymentType::VENMO,
|
||||
"28" => $method = PaymentType::PAY_LATER,
|
||||
};
|
||||
|
||||
return $method;
|
||||
}
|
||||
|
||||
public function setPaymentMethod($payment_method_id): self
|
||||
{
|
||||
if(!$payment_method_id) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->gateway_type_id = $payment_method_id;
|
||||
|
||||
$this->paypal_payment_method = $this->funding_options[$payment_method_id];
|
||||
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@ -122,7 +165,9 @@ class PayPalRestPaymentDriver extends BaseDriver
|
||||
$data['client_id'] = $this->company_gateway->getConfigField('clientId');
|
||||
$data['token'] = $this->getClientToken();
|
||||
$data['order_id'] = $this->createOrder($data);
|
||||
$data['funding_options'] = $this->paypal_payment_method;
|
||||
$data['funding_source'] = $this->paypal_payment_method;
|
||||
$data['gateway_type_id'] = $this->gateway_type_id;
|
||||
$data['currency'] = $this->client->currency()->code;
|
||||
|
||||
return render('gateways.paypal.pay', $data);
|
||||
|
||||
@ -166,13 +211,43 @@ class PayPalRestPaymentDriver extends BaseDriver
|
||||
public function processPaymentResponse($request)
|
||||
{
|
||||
|
||||
$this->init();
|
||||
|
||||
$request['gateway_response'] = str_replace("Error: ", "", $request['gateway_response']);
|
||||
$response = json_decode($request['gateway_response'], true);
|
||||
|
||||
if($response['status'] == 'COMPLETED' && isset($response['purchase_units'])) {
|
||||
//capture
|
||||
$orderID = $response['orderID'];
|
||||
|
||||
if($this->company_gateway->require_shipping_address) {
|
||||
|
||||
$shipping_data =
|
||||
[[
|
||||
"op" => "replace",
|
||||
"path" => "/purchase_units/@reference_id=='default'/shipping/address",
|
||||
"value" => [
|
||||
"address_line_1" => strlen($this->client->shipping_address1) > 1 ? $this->client->shipping_address1 : $this->client->address1,
|
||||
"address_line_2" => $this->client->shipping_address2,
|
||||
"admin_area_2" => strlen($this->client->shipping_city) > 1 ? $this->client->shipping_city : $this->client->city,
|
||||
"admin_area_1" => strlen($this->client->shipping_state) > 1 ? $this->client->shipping_state : $this->client->state,
|
||||
"postal_code" => strlen($this->client->shipping_postal_code) > 1 ? $this->client->shipping_postal_code : $this->client->postal_code,
|
||||
"country_code" => $this->client->present()->shipping_country_code(),
|
||||
],
|
||||
]];
|
||||
|
||||
$r = $this->gatewayRequest("/v2/checkout/orders/{$orderID}", 'patch', $shipping_data);
|
||||
|
||||
}
|
||||
|
||||
$r = $this->gatewayRequest("/v2/checkout/orders/{$orderID}/capture", 'post', ['body' => '']);
|
||||
|
||||
$response = $r;
|
||||
|
||||
if(isset($response['status']) && $response['status'] == 'COMPLETED' && isset($response['purchase_units'])) {
|
||||
|
||||
$data = [
|
||||
'payment_type' => PaymentType::PAYPAL,
|
||||
'amount' => $response['purchase_units'][0]['amount']['value'],
|
||||
'payment_type' => $this->getPaymentMethod($request->gateway_type_id),
|
||||
'amount' => $response['purchase_units'][0]['payments']['captures'][0]['amount']['value'],
|
||||
'transaction_reference' => $response['purchase_units'][0]['payments']['captures'][0]['id'],
|
||||
'gateway_type_id' => GatewayType::PAYPAL,
|
||||
];
|
||||
@ -192,6 +267,10 @@ class PayPalRestPaymentDriver extends BaseDriver
|
||||
|
||||
} else {
|
||||
|
||||
if(isset($response['headers']) ?? false) {
|
||||
unset($response['headers']);
|
||||
}
|
||||
|
||||
SystemLogger::dispatch(
|
||||
['response' => $response],
|
||||
SystemLog::CATEGORY_GATEWAY_RESPONSE,
|
||||
@ -201,9 +280,11 @@ class PayPalRestPaymentDriver extends BaseDriver
|
||||
$this->client->company,
|
||||
);
|
||||
|
||||
$message = $response['body']['details'][0]['description'] ?? 'Payment failed. Please try again.';
|
||||
|
||||
throw new PaymentFailed('Payment failed. Please try again.', 401);
|
||||
throw new PaymentFailed($message, 400);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private function getClientToken(): string
|
||||
@ -226,60 +307,169 @@ class PayPalRestPaymentDriver extends BaseDriver
|
||||
|
||||
$invoice = Invoice::withTrashed()->find($this->decodePrimaryKey($_invoice->invoice_id));
|
||||
|
||||
$description = collect($invoice->line_items)->map(function ($item) {
|
||||
return $item->notes;
|
||||
})->implode("\n");
|
||||
|
||||
$order = [
|
||||
"intent" => "CAPTURE",
|
||||
"payer" => [
|
||||
"name" => [
|
||||
"given_name" => $this->client->present()->first_name(),
|
||||
"surname" => $this->client->present()->last_name(),
|
||||
],
|
||||
"email_address" => $this->client->present()->email(),
|
||||
"address" => [
|
||||
"address_line_1" => $this->client->address1,
|
||||
"address_line_2" => $this->client->address2,
|
||||
"admin_area_1" => $this->client->city,
|
||||
"admin_area_2" => $this->client->state,
|
||||
"postal_code" => $this->client->postal_code,
|
||||
"country_code" => $this->client->country->iso_3166_2,
|
||||
]
|
||||
],
|
||||
"purchase_units" => [
|
||||
[
|
||||
"description" => ctrans('texts.invoice_number').'# '.$invoice->number,
|
||||
"invoice_id" => $invoice->number,
|
||||
"amount" => [
|
||||
"value" => (string)$data['amount_with_fee'],
|
||||
"currency_code" => $this->client->currency()->code,
|
||||
"breakdown" => [
|
||||
"item_total" => [
|
||||
"currency_code" => $this->client->currency()->code,
|
||||
"value" => (string)$data['amount_with_fee']
|
||||
]
|
||||
]
|
||||
],
|
||||
"items" => [
|
||||
[
|
||||
"name" => ctrans('texts.invoice_number').'# '.$invoice->number,
|
||||
"quantity" => "1",
|
||||
"unit_amount" => [
|
||||
"currency_code" => $this->client->currency()->code,
|
||||
"value" => (string)$data['amount_with_fee']
|
||||
|
||||
"intent" => "CAPTURE",
|
||||
"payment_source" => [
|
||||
"paypal" => [
|
||||
|
||||
"name" => [
|
||||
"given_name" => $this->client->present()->first_name(),
|
||||
"surname" => $this->client->present()->last_name(),
|
||||
],
|
||||
"email_address" => $this->client->present()->email(),
|
||||
"address" => [
|
||||
"address_line_1" => $this->client->address1,
|
||||
"address_line_2" => $this->client->address2,
|
||||
"admin_area_2" => $this->client->city,
|
||||
"admin_area_1" => $this->client->state,
|
||||
"postal_code" => $this->client->postal_code,
|
||||
"country_code" => $this->client->country->iso_3166_2,
|
||||
],
|
||||
"experience_context" => [
|
||||
"user_action" => "PAY_NOW"
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
]
|
||||
];
|
||||
"purchase_units" => [
|
||||
[
|
||||
"custom_id" => $this->payment_hash->hash,
|
||||
"description" => ctrans('texts.invoice_number') . '# ' . $invoice->number,
|
||||
"invoice_id" => $invoice->number,
|
||||
"payment_instruction" => [
|
||||
"disbursement_mode" => "INSTANT",
|
||||
],
|
||||
$this->getShippingAddress(),
|
||||
"amount" => [
|
||||
"value" => (string) $data['amount_with_fee'],
|
||||
"currency_code" => $this->client->currency()->code,
|
||||
"breakdown" => [
|
||||
"item_total" => [
|
||||
"currency_code" => $this->client->currency()->code,
|
||||
"value" => (string) $data['amount_with_fee']
|
||||
]
|
||||
]
|
||||
],
|
||||
"items" => [
|
||||
[
|
||||
"name" => ctrans('texts.invoice_number') . '# ' . $invoice->number,
|
||||
"description" => mb_substr($description, 0, 127),
|
||||
"quantity" => "1",
|
||||
"unit_amount" => [
|
||||
"currency_code" => $this->client->currency()->code,
|
||||
"value" => (string) $data['amount_with_fee']
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
];
|
||||
|
||||
|
||||
if($shipping = $this->getShippingAddress()) {
|
||||
$order['purchase_units'][0]["shipping"] = $shipping;
|
||||
}
|
||||
|
||||
$r = $this->gatewayRequest('/v2/checkout/orders', 'post', $order);
|
||||
|
||||
// nlog($r->json());
|
||||
|
||||
return $r->json()['id'];
|
||||
|
||||
|
||||
|
||||
// $_invoice = collect($this->payment_hash->data->invoices)->first();
|
||||
|
||||
// $invoice = Invoice::withTrashed()->find($this->decodePrimaryKey($_invoice->invoice_id));
|
||||
|
||||
// $order = [
|
||||
// "intent" => "CAPTURE",
|
||||
// "payer" => [
|
||||
// "name" => [
|
||||
// "given_name" => $this->client->present()->first_name(),
|
||||
// "surname" => $this->client->present()->last_name(),
|
||||
// ],
|
||||
// "email_address" => $this->client->present()->email(),
|
||||
// "address" => [
|
||||
// "address_line_1" => $this->client->address1,
|
||||
// "address_line_2" => $this->client->address2,
|
||||
// "admin_area_1" => $this->client->city,
|
||||
// "admin_area_2" => $this->client->state,
|
||||
// "postal_code" => $this->client->postal_code,
|
||||
// "country_code" => $this->client->country->iso_3166_2,
|
||||
// ]
|
||||
// ],
|
||||
// "purchase_units" => [
|
||||
// [
|
||||
// "description" => ctrans('texts.invoice_number').'# '.$invoice->number,
|
||||
// "invoice_id" => $invoice->number,
|
||||
// "amount" => [
|
||||
// "value" => (string)$data['amount_with_fee'],
|
||||
// "currency_code" => $this->client->currency()->code,
|
||||
// "breakdown" => [
|
||||
// "item_total" => [
|
||||
// "currency_code" => $this->client->currency()->code,
|
||||
// "value" => (string)$data['amount_with_fee']
|
||||
// ]
|
||||
// ]
|
||||
// ],
|
||||
// "items" => [
|
||||
// [
|
||||
// "name" => ctrans('texts.invoice_number').'# '.$invoice->number,
|
||||
// "quantity" => "1",
|
||||
// "unit_amount" => [
|
||||
// "currency_code" => $this->client->currency()->code,
|
||||
// "value" => (string)$data['amount_with_fee']
|
||||
// ],
|
||||
// ],
|
||||
// ],
|
||||
// ]
|
||||
// ]
|
||||
// ];
|
||||
|
||||
// $r = $this->gatewayRequest('/v2/checkout/orders', 'post', $order);
|
||||
|
||||
// return $r->json()['id'];
|
||||
|
||||
}
|
||||
|
||||
private function getShippingAddress(): ?array
|
||||
{
|
||||
return $this->company_gateway->require_shipping_address ?
|
||||
[
|
||||
"address" =>
|
||||
[
|
||||
"address_line_1" => strlen($this->client->shipping_address1) > 1 ? $this->client->shipping_address1 : $this->client->address1,
|
||||
"address_line_2" => $this->client->shipping_address2,
|
||||
"admin_area_2" => strlen($this->client->shipping_city) > 1 ? $this->client->shipping_city : $this->client->city,
|
||||
"admin_area_1" => strlen($this->client->shipping_state) > 1 ? $this->client->shipping_state : $this->client->state,
|
||||
"postal_code" => strlen($this->client->shipping_postal_code) > 1 ? $this->client->shipping_postal_code : $this->client->postal_code,
|
||||
"country_code" => $this->client->present()->shipping_country_code(),
|
||||
],
|
||||
]
|
||||
|
||||
: null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the gateway request
|
||||
*
|
||||
* @param string $uri
|
||||
* @param string $verb
|
||||
* @param array $data
|
||||
* @param ?array $headers
|
||||
* @return \Illuminate\Http\Client\Response
|
||||
*/
|
||||
public function gatewayRequest(string $uri, string $verb, array $data, ?array $headers = [])
|
||||
{
|
||||
$r = Http::withToken($this->omnipay_gateway->getToken())
|
||||
$this->init();
|
||||
|
||||
$r = Http::withToken($this->access_token)
|
||||
->withHeaders($this->getHeaders($headers))
|
||||
->{$verb}("{$this->api_endpoint_url}{$uri}", $data);
|
||||
|
||||
@ -287,6 +477,15 @@ class PayPalRestPaymentDriver extends BaseDriver
|
||||
return $r;
|
||||
}
|
||||
|
||||
SystemLogger::dispatch(
|
||||
['response' => $r->body()],
|
||||
SystemLog::CATEGORY_GATEWAY_RESPONSE,
|
||||
SystemLog::EVENT_GATEWAY_FAILURE,
|
||||
SystemLog::TYPE_PAYPAL,
|
||||
$this->client,
|
||||
$this->client->company,
|
||||
);
|
||||
|
||||
throw new PaymentFailed("Gateway failure - {$r->body()}", 401);
|
||||
|
||||
}
|
||||
|
@ -10,6 +10,7 @@
|
||||
<input type="hidden" name="payment_hash" value="{{ $payment_hash }}">
|
||||
<input type="hidden" name="company_gateway_id" value="{{ $gateway->company_gateway->id }}">
|
||||
<input type="hidden" name="gateway_response" id="gateway_response">
|
||||
<input type="hidden" name="gateway_type_id" id="gateway_type_id" value="{{ $gateway_type_id }}">
|
||||
<input type="hidden" name="amount_with_fee" id="amount_with_fee" value="{{ $total['amount_with_fee'] }}"/>
|
||||
</form>
|
||||
|
||||
@ -23,52 +24,62 @@
|
||||
@endsection
|
||||
|
||||
@push('footer')
|
||||
<script src="https://www.paypal.com/sdk/js?enable-funding={!! $funding_options !!}&disable-funding=credit&components=buttons,hosted-fields,funding-eligibility&intent=capture&client-id={!! $client_id !!}" data-client-token="{!! $token !!}" data-partner-attribution-id="invoiceninja_SP_PPCP">
|
||||
</script>
|
||||
|
||||
<script src="https://www.paypal.com/sdk/js?client-id={!! $client_id !!}¤cy={!! $currency !!}&components=buttons,funding-eligibility&intent=capture&enable-funding={!! $funding_source !!}" data-partner-attribution-id="invoiceninja_SP_PPCP"></script>
|
||||
<div id="paypal-button-container"></div>
|
||||
<script>
|
||||
|
||||
paypal.Buttons({
|
||||
fundingSource: "{{ $funding_options }}",
|
||||
env: "{{ $gateway->company_gateway->getConfigField('testMode') ? 'sandbox' : 'production' }}",
|
||||
client: {
|
||||
@if($gateway->company_gateway->getConfigField('testMode'))
|
||||
sandbox: "{{ $gateway->company_gateway->getConfigField('clientId') }}"
|
||||
@else
|
||||
production: "{{ $gateway->company_gateway->getConfigField('clientId') }}"
|
||||
@endif
|
||||
},
|
||||
createOrder: function(data, actions) {
|
||||
return "{!! $order_id !!}"
|
||||
},
|
||||
onCancel: function() {
|
||||
window.location.href = "/client/invoices/";
|
||||
},
|
||||
onApprove: function(data, actions) {
|
||||
//&buyer-country=US¤cy=USD&enable-funding=venmo
|
||||
const fundingSource = "{!! $funding_source !!}";
|
||||
const clientId = "{{ $client_id }}";
|
||||
const orderId = "{!! $order_id !!}";
|
||||
const environment = "{{ $gateway->company_gateway->getConfigField('testMode') ? 'sandbox' : 'production' }}";
|
||||
|
||||
return actions.order.capture().then(function(details) {
|
||||
|
||||
document.getElementById("gateway_response").value =JSON.stringify( details );
|
||||
document.getElementById("server_response").submit();
|
||||
paypal.Buttons({
|
||||
env: environment,
|
||||
fundingSource: fundingSource,
|
||||
client: clientId,
|
||||
createOrder: function(data, actions) {
|
||||
return orderId;
|
||||
},
|
||||
onApprove: function(data, actions) {
|
||||
|
||||
});
|
||||
var errorDetail = Array.isArray(data.details) && data.details[0];
|
||||
if (errorDetail && ['INSTRUMENT_DECLINED', 'PAYER_ACTION_REQUIRED'].includes(errorDetail.issue)) {
|
||||
return actions.restart();
|
||||
}
|
||||
|
||||
},
|
||||
onError: function(err) {
|
||||
console.log(err);
|
||||
},
|
||||
onClick: function (){
|
||||
document.getElementById('paypal-button-container').hidden = true;
|
||||
}
|
||||
document.getElementById("gateway_response").value =JSON.stringify( data );
|
||||
document.getElementById("server_response").submit();
|
||||
|
||||
},
|
||||
onCancel: function() {
|
||||
window.location.href = "/client/invoices/";
|
||||
},
|
||||
onError: function(error) {
|
||||
document.getElementById("gateway_response").value = error;
|
||||
document.getElementById("server_response").submit();
|
||||
},
|
||||
onClick: function (){
|
||||
|
||||
if(fundingSource != 'card')
|
||||
document.getElementById('paypal-button-container').hidden = true;
|
||||
}
|
||||
|
||||
}).render('#paypal-button-container').catch(function(err) {
|
||||
|
||||
document.getElementById('errors').textContent = err;
|
||||
document.getElementById('errors').hidden = false;
|
||||
|
||||
})
|
||||
|
||||
|
||||
});
|
||||
|
||||
document.getElementById("server_response").addEventListener('submit', (e) => {
|
||||
if (document.getElementById("server_response").classList.contains('is-submitting')) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
document.getElementById("server_response").classList.add('is-submitting');
|
||||
});
|
||||
|
||||
</script>
|
||||
@endpush
|
Loading…
x
Reference in New Issue
Block a user