Adjustments for PayPal Rest implementation

This commit is contained in:
David Bomba 2024-01-28 09:21:45 +11:00
parent d05a58cebe
commit 9f5cfd7440
3 changed files with 306 additions and 99 deletions

View File

@ -519,9 +519,6 @@ class PayPalPPCPPaymentDriver extends BaseDriver
->withHeaders($this->getHeaders($headers)) ->withHeaders($this->getHeaders($headers))
->{$verb}("{$this->api_endpoint_url}{$uri}", $data); ->{$verb}("{$this->api_endpoint_url}{$uri}", $data);
// nlog($r);
// nlog($r->json());
if($r->successful()) { if($r->successful()) {
return $r; return $r;
} }

View File

@ -12,15 +12,16 @@
namespace App\PaymentDrivers; namespace App\PaymentDrivers;
use App\Exceptions\PaymentFailed; use Carbon\Carbon;
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 Omnipay\Omnipay; 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 class PayPalRestPaymentDriver extends BaseDriver
{ {
@ -40,6 +41,12 @@ class PayPalRestPaymentDriver extends BaseDriver
private string $paypal_payment_method = ''; 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 = [ private array $funding_options = [
3 => 'paypal', 3 => 'paypal',
1 => 'card', 1 => 'card',
@ -51,7 +58,7 @@ class PayPalRestPaymentDriver extends BaseDriver
// 13 => 'ideal', // 13 => 'ideal',
// 26 => 'mercadopago', // 26 => 'mercadopago',
// 27 => 'mybank', // 27 => 'mybank',
// 28 => 'paylater', 28 => 'paylater',
// 16 => 'p24', // 16 => 'p24',
// 7 => 'sofort' // 7 => 'sofort'
]; ];
@ -74,25 +81,61 @@ class PayPalRestPaymentDriver extends BaseDriver
public function init() public function init()
{ {
$this->omnipay_gateway = Omnipay::create( // $this->omnipay_gateway = Omnipay::create(
$this->company_gateway->gateway->provider // $this->company_gateway->gateway->provider
); // );
$this->omnipay_gateway->initialize((array) $this->company_gateway->getConfig());
// $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'; $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; 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) { if(!$payment_method_id) {
return $this; return $this;
} }
$this->gateway_type_id = $payment_method_id;
$this->paypal_payment_method = $this->funding_options[$payment_method_id]; $this->paypal_payment_method = $this->funding_options[$payment_method_id];
return $this; return $this;
} }
@ -122,7 +165,9 @@ class PayPalRestPaymentDriver extends BaseDriver
$data['client_id'] = $this->company_gateway->getConfigField('clientId'); $data['client_id'] = $this->company_gateway->getConfigField('clientId');
$data['token'] = $this->getClientToken(); $data['token'] = $this->getClientToken();
$data['order_id'] = $this->createOrder($data); $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); return render('gateways.paypal.pay', $data);
@ -166,13 +211,43 @@ class PayPalRestPaymentDriver extends BaseDriver
public function processPaymentResponse($request) public function processPaymentResponse($request)
{ {
$this->init();
$request['gateway_response'] = str_replace("Error: ", "", $request['gateway_response']);
$response = json_decode($request['gateway_response'], true); $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 = [ $data = [
'payment_type' => PaymentType::PAYPAL, 'payment_type' => $this->getPaymentMethod($request->gateway_type_id),
'amount' => $response['purchase_units'][0]['amount']['value'], 'amount' => $response['purchase_units'][0]['payments']['captures'][0]['amount']['value'],
'transaction_reference' => $response['purchase_units'][0]['payments']['captures'][0]['id'], 'transaction_reference' => $response['purchase_units'][0]['payments']['captures'][0]['id'],
'gateway_type_id' => GatewayType::PAYPAL, 'gateway_type_id' => GatewayType::PAYPAL,
]; ];
@ -192,6 +267,10 @@ class PayPalRestPaymentDriver extends BaseDriver
} else { } else {
if(isset($response['headers']) ?? false) {
unset($response['headers']);
}
SystemLogger::dispatch( SystemLogger::dispatch(
['response' => $response], ['response' => $response],
SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::CATEGORY_GATEWAY_RESPONSE,
@ -201,9 +280,11 @@ class PayPalRestPaymentDriver extends BaseDriver
$this->client->company, $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 private function getClientToken(): string
@ -226,60 +307,169 @@ class PayPalRestPaymentDriver extends BaseDriver
$invoice = Invoice::withTrashed()->find($this->decodePrimaryKey($_invoice->invoice_id)); $invoice = Invoice::withTrashed()->find($this->decodePrimaryKey($_invoice->invoice_id));
$description = collect($invoice->line_items)->map(function ($item) {
return $item->notes;
})->implode("\n");
$order = [ $order = [
"intent" => "CAPTURE",
"payer" => [ "intent" => "CAPTURE",
"name" => [ "payment_source" => [
"given_name" => $this->client->present()->first_name(), "paypal" => [
"surname" => $this->client->present()->last_name(),
], "name" => [
"email_address" => $this->client->present()->email(), "given_name" => $this->client->present()->first_name(),
"address" => [ "surname" => $this->client->present()->last_name(),
"address_line_1" => $this->client->address1, ],
"address_line_2" => $this->client->address2, "email_address" => $this->client->present()->email(),
"admin_area_1" => $this->client->city, "address" => [
"admin_area_2" => $this->client->state, "address_line_1" => $this->client->address1,
"postal_code" => $this->client->postal_code, "address_line_2" => $this->client->address2,
"country_code" => $this->client->country->iso_3166_2, "admin_area_2" => $this->client->city,
] "admin_area_1" => $this->client->state,
], "postal_code" => $this->client->postal_code,
"purchase_units" => [ "country_code" => $this->client->country->iso_3166_2,
[ ],
"description" => ctrans('texts.invoice_number').'# '.$invoice->number, "experience_context" => [
"invoice_id" => $invoice->number, "user_action" => "PAY_NOW"
"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']
], ],
], ],
], "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); $r = $this->gatewayRequest('/v2/checkout/orders', 'post', $order);
// nlog($r->json());
return $r->json()['id']; 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 = []) 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)) ->withHeaders($this->getHeaders($headers))
->{$verb}("{$this->api_endpoint_url}{$uri}", $data); ->{$verb}("{$this->api_endpoint_url}{$uri}", $data);
@ -287,6 +477,15 @@ class PayPalRestPaymentDriver extends BaseDriver
return $r; 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); throw new PaymentFailed("Gateway failure - {$r->body()}", 401);
} }

View File

@ -10,6 +10,7 @@
<input type="hidden" name="payment_hash" value="{{ $payment_hash }}"> <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="company_gateway_id" value="{{ $gateway->company_gateway->id }}">
<input type="hidden" name="gateway_response" id="gateway_response"> <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'] }}"/> <input type="hidden" name="amount_with_fee" id="amount_with_fee" value="{{ $total['amount_with_fee'] }}"/>
</form> </form>
@ -23,52 +24,62 @@
@endsection @endsection
@push('footer') @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 !!}&currency={!! $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> <script>
paypal.Buttons({ //&buyer-country=US&currency=USD&enable-funding=venmo
fundingSource: "{{ $funding_options }}", const fundingSource = "{!! $funding_source !!}";
env: "{{ $gateway->company_gateway->getConfigField('testMode') ? 'sandbox' : 'production' }}", const clientId = "{{ $client_id }}";
client: { const orderId = "{!! $order_id !!}";
@if($gateway->company_gateway->getConfigField('testMode')) const environment = "{{ $gateway->company_gateway->getConfigField('testMode') ? 'sandbox' : 'production' }}";
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) {
return actions.order.capture().then(function(details) { paypal.Buttons({
env: environment,
document.getElementById("gateway_response").value =JSON.stringify( details ); fundingSource: fundingSource,
document.getElementById("server_response").submit(); 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();
}
}, document.getElementById("gateway_response").value =JSON.stringify( data );
onError: function(err) { document.getElementById("server_response").submit();
console.log(err);
}, },
onClick: function (){ onCancel: function() {
document.getElementById('paypal-button-container').hidden = true; 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) { }).render('#paypal-button-container').catch(function(err) {
document.getElementById('errors').textContent = err; document.getElementById('errors').textContent = err;
document.getElementById('errors').hidden = false; 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> </script>
@endpush @endpush