1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-15 07:33:04 +01:00
invoiceninja/app/PaymentDrivers/PayPal/PayPalBasePaymentDriver.php

599 lines
20 KiB
PHP
Raw Normal View History

2024-05-20 03:59:44 +02:00
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\PaymentDrivers\PayPal;
use Str;
use Carbon\Carbon;
use App\Models\Invoice;
use App\Models\SystemLog;
use App\Models\GatewayType;
2024-06-13 08:52:23 +02:00
use App\Models\PaymentHash;
2024-05-20 03:59:44 +02:00
use App\Models\PaymentType;
use Illuminate\Http\Request;
use App\Jobs\Util\SystemLogger;
use App\Utils\Traits\MakesHash;
use App\Exceptions\PaymentFailed;
use App\Models\ClientGatewayToken;
use App\PaymentDrivers\BaseDriver;
use Illuminate\Support\Facades\Http;
use App\PaymentDrivers\PayPal\PayPalWebhook;
class PayPalBasePaymentDriver extends BaseDriver
{
use MakesHash;
2024-06-13 08:52:23 +02:00
public string $risk_guid;
2024-05-20 03:59:44 +02:00
public $token_billing = true;
public $can_authorise_credit_card = false;
public float $fee = 0;
public const SYSTEM_LOG_TYPE = SystemLog::TYPE_PAYPAL;
public string $api_endpoint_url = '';
public string $paypal_payment_method = '';
public ?int $gateway_type_id = null;
public mixed $access_token = null;
public ?Carbon $token_expiry = null;
public array $funding_options = [
3 => 'paypal',
1 => 'card',
25 => 'venmo',
29 => 'paypal_advanced_cards',
// 9 => 'sepa',
// 12 => 'bancontact',
// 17 => 'eps',
// 15 => 'giropay',
// 13 => 'ideal',
// 26 => 'mercadopago',
// 27 => 'mybank',
28 => 'paylater',
// 16 => 'p24',
// 7 => 'sofort'
];
public function gatewayTypes()
{
2024-06-14 09:09:44 +02:00
$funding_options =
2024-05-20 03:59:44 +02:00
collect($this->company_gateway->fees_and_limits)
->filter(function ($fee) {
return $fee->is_enabled;
})->map(function ($fee, $key) {
return (int)$key;
})->toArray();
/** Parse funding options and remove card option if advanced cards is enabled. */
2024-06-14 09:09:44 +02:00
if(in_array(1, $funding_options) && in_array(29, $funding_options)) {
2024-05-20 03:59:44 +02:00
2024-06-14 09:09:44 +02:00
if (($key = array_search(1, $funding_options)) !== false) {
2024-05-20 03:59:44 +02:00
unset($funding_options[$key]);
2024-06-14 09:09:44 +02:00
}
2024-05-20 03:59:44 +02:00
}
return $funding_options;
}
public 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,
"29" => $method = PaymentType::CREDIT_CARD_OTHER,
2024-06-18 01:41:22 +02:00
default => $method = PaymentType::PAYPAL,
2024-05-20 03:59:44 +02:00
};
return $method;
}
public function init()
{
2024-06-13 08:52:23 +02:00
$this->risk_guid = Str::random(32);
2024-05-20 03:59:44 +02:00
$this->api_endpoint_url = $this->company_gateway->getConfigField('testMode') ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com';
2024-08-07 03:09:56 +02:00
if(\App\Utils\Ninja::isHosted() && $this->company_gateway->gateway_key != '80af24a6a691230bbec33e930ab40665') {
2024-06-16 09:22:15 +02:00
$secret = config('ninja.paypal.secret');
$client_id = config('ninja.paypal.client_id');
}
else {
$secret = $this->company_gateway->getConfigField('secret');
$client_id = $this->company_gateway->getConfigField('clientId');
}
2024-05-20 03:59:44 +02:00
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;
}
/**
* getFundingOptions
*
* Hosted fields requires this.
2024-06-14 09:09:44 +02:00
*
2024-05-20 03:59:44 +02:00
* @return string
*/
public function getFundingOptions(): string
{
$enums = [
1 => 'card',
3 => 'paypal',
25 => 'venmo',
28 => 'paylater',
// 9 => 'sepa',
// 12 => 'bancontact',
// 17 => 'eps',
// 15 => 'giropay',
// 13 => 'ideal',
// 26 => 'mercadopago',
// 27 => 'mybank',
// 28 => 'paylater',
// 16 => 'p24',
// 7 => 'sofort'
];
$funding_options = '';
foreach($this->company_gateway->fees_and_limits as $key => $value) {
if($value->is_enabled) {
$funding_options .= $enums[$key].',';
}
}
return rtrim($funding_options, ',');
}
2024-06-14 09:07:52 +02:00
//@todo turn this back on when PayPal.....
2024-06-13 11:59:29 +02:00
public function getClientHash()
{
2024-06-14 09:07:52 +02:00
return '';
2024-06-17 12:38:33 +02:00
/** @var ?\App\Models\ClientGatewayToken $cgt */
2024-06-13 11:59:29 +02:00
$cgt = ClientGatewayToken::where('company_gateway_id', $this->company_gateway->id)
->where('client_id', $this->client->id)
->first();
2024-06-14 09:09:44 +02:00
if(!$cgt) {
2024-06-13 11:59:29 +02:00
return '';
2024-06-14 09:09:44 +02:00
}
2024-06-13 11:59:29 +02:00
$client_reference = $cgt->gateway_customer_reference;
$secret = $this->company_gateway->getConfigField('secret');
$client_id = $this->company_gateway->getConfigField('clientId');
2024-06-14 09:09:44 +02:00
$response = Http::withBasicAuth($client_id, $secret)
->withHeaders(['Content-Type' => 'application/x-www-form-urlencoded'])
->withQueryParameters(['grant_type' => 'client_credentials','response_type' => 'id_token', 'target_customer_id' => $client_reference])
->post("{$this->api_endpoint_url}/v1/oauth2/token");
2024-06-13 11:59:29 +02:00
if($response->successful()) {
2024-06-14 09:09:44 +02:00
$data = $response->json();
2024-06-13 11:59:29 +02:00
return $data['id_token'] ?? '';
}
return '';
}
2024-06-06 04:31:24 +02:00
public function handleDuplicateInvoiceId(string $orderID)
{
$_invoice = collect($this->payment_hash->data->invoices)->first();
$invoice = Invoice::withTrashed()->find($this->decodePrimaryKey($_invoice->invoice_id));
$new_invoice_number = $invoice->number."_".Str::random(5);
$update_data =
[[
"op" => "replace",
"path" => "/purchase_units/@reference_id=='default'/invoice_id",
"value" => $new_invoice_number,
]];
$r = $this->gatewayRequest("/v2/checkout/orders/{$orderID}", 'patch', $update_data);
$r = $this->gatewayRequest("/v2/checkout/orders/{$orderID}/capture", 'post', ['body' => '']);
return $r;
}
2024-06-14 09:09:44 +02:00
2024-05-20 03:59:44 +02:00
public function getShippingAddress(): ?array
{
return $this->company_gateway->require_shipping_address ?
[
"address" =>
[
2024-07-28 00:51:24 +02:00
"address_line_1" => strlen($this->client->shipping_address1 ?? '') > 1 ? $this->client->shipping_address1 : $this->client->address1,
2024-05-20 03:59:44 +02:00
"address_line_2" => $this->client->shipping_address2,
2024-07-28 00:51:24 +02:00
"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,
2024-05-20 03:59:44 +02:00
"country_code" => $this->client->present()->shipping_country_code(),
],
]
: [
"name" => [
"full_name" => $this->client->present()->name()
]
];
}
public function getBillingAddress(): array
{
return
[
"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,
];
}
2024-06-14 09:09:44 +02:00
2024-05-20 03:59:44 +02:00
public function getPaymentSource(): array
{
//@todo - roll back here for advanced payments vs hosted card fields.
if($this->gateway_type_id == GatewayType::PAYPAL_ADVANCED_CARDS) {
return [
"card" => [
"attributes" => [
"verification" => [
"method" => "SCA_WHEN_REQUIRED", //SCA_ALWAYS
// "method" => "SCA_ALWAYS", //SCA_ALWAYS
],
"vault" => [
"store_in_vault" => "ON_SUCCESS", //must listen to this webhook - VAULT.PAYMENT-TOKEN.CREATED webhook.
],
],
"experience_context" => [
"shipping_preference" => "SET_PROVIDED_ADDRESS"
],
"stored_credential" => [
// "payment_initiator" => "MERCHANT", //"CUSTOMER" who initiated the transaction?
"payment_initiator" => "CUSTOMER", //"" who initiated the transaction?
"payment_type" => "UNSCHEDULED", //UNSCHEDULED
2024-06-14 09:09:44 +02:00
"usage" => "DERIVED",
2024-05-20 03:59:44 +02:00
],
],
];
}
$order = [
"paypal" => [
"name" => [
"given_name" => $this->client->present()->first_name(),
"surname" => $this->client->present()->last_name(),
],
"email_address" => $this->client->present()->email(),
"experience_context" => [
"user_action" => "PAY_NOW"
],
],
];
/** If we have a complete address, add it to the order, otherwise leave it blank! */
if(
strlen($this->client->shipping_address1 ?? '') > 2 &&
strlen($this->client->shipping_city ?? '') > 2 &&
strlen($this->client->shipping_state ?? '') >= 2 &&
strlen($this->client->shipping_postal_code ?? '') > 2 &&
strlen($this->client->shipping_country->iso_3166_2 ?? '') >= 2
) {
$order['paypal']['address'] = [
"address_line_1" => $this->client->shipping_address1,
"address_line_2" => $this->client->shipping_address2,
"admin_area_2" => $this->client->shipping_city,
"admin_area_1" => $this->client->shipping_state,
"postal_code" => $this->client->shipping_postal_code,
"country_code" => $this->client->present()->shipping_country_code(),
];
2024-06-14 09:09:44 +02:00
} elseif(
2024-05-20 03:59:44 +02:00
strlen($this->client->address1 ?? '') > 2 &&
strlen($this->client->city ?? '') > 2 &&
strlen($this->client->state ?? '') >= 2 &&
strlen($this->client->postal_code ?? '') > 2 &&
strlen($this->client->country->iso_3166_2 ?? '') >= 2
2024-06-14 09:09:44 +02:00
) {
2024-05-20 03:59:44 +02:00
$order['paypal']['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,
];
}
return $order;
}
2024-06-14 09:09:44 +02:00
2024-05-20 03:59:44 +02:00
/**
* Payment method setter
*
* @param mixed $payment_method_id
* @return self
*/
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;
}
public function authorizeView($payment_method)
{
// PayPal doesn't support direct authorization.
return $this;
}
public function authorizeResponse($request)
{
// PayPal doesn't support direct authorization.
return $this;
}
/**
* 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 = [])
{
$this->init();
$r = Http::withToken($this->access_token)
->withHeaders($this->getHeaders($headers))
->{$verb}("{$this->api_endpoint_url}{$uri}", $data);
2024-06-14 09:09:44 +02:00
if($r->status() <= 422) {
// if($r->successful()) {
2024-05-20 03:59:44 +02:00
return $r;
}
2024-05-24 02:38:07 +02:00
nlog($r->body());
nlog($r->json());
nlog($r);
2024-05-20 03:59:44 +02:00
SystemLogger::dispatch(
['response' => $r->body()],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::TYPE_PAYPAL,
$this->client,
$this->client->company ?? $this->company_gateway->company,
);
2024-06-14 09:09:44 +02:00
2024-05-24 02:38:07 +02:00
return response()->json(['message' => "Gateway failure - {$r->body()}"], 401);
// throw new PaymentFailed("Gateway failure - {$r->body()}", 401);
2024-05-20 03:59:44 +02:00
}
2024-06-14 09:09:44 +02:00
2024-06-06 03:17:34 +02:00
public function handleProcessingFailure(array $response)
{
2024-05-24 02:38:07 +02:00
2024-06-06 03:17:34 +02:00
SystemLogger::dispatch(
['response' => $response],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::TYPE_PAYPAL,
$this->client,
$this->client->company ?? $this->company_gateway->company,
);
2024-05-24 02:38:07 +02:00
2024-06-06 03:17:34 +02:00
switch ($response['name']) {
case 'NOT_AUTHORIZED':
throw new PaymentFailed("There was a permissions issue processing this payment, please contact the merchant. ", 401);
2024-06-17 08:08:06 +02:00
2024-06-14 09:09:44 +02:00
2024-06-06 03:17:34 +02:00
default:
throw new PaymentFailed("Unknown error occurred processing payment. Please contact merchant.", 500);
2024-06-17 08:08:06 +02:00
2024-06-06 03:17:34 +02:00
}
}
2024-06-14 09:09:44 +02:00
public function handleRetry($response, $request)
{
2024-05-24 02:38:07 +02:00
return response()->json($response->json());
}
2024-05-20 03:59:44 +02:00
/**
* Generates the request headers
*
* @param array $headers
* @return array
*/
public function getHeaders(array $headers = []): array
{
2024-05-24 02:38:07 +02:00
2024-05-20 03:59:44 +02:00
return array_merge([
'Accept' => 'application/json',
'Content-type' => 'application/json',
'Accept-Language' => 'en_US',
'PayPal-Partner-Attribution-Id' => 'invoiceninja_SP_PPCP',
'PayPal-Request-Id' => Str::uuid()->toString(),
2024-06-13 08:52:23 +02:00
'PAYPAL-CLIENT-METADATA-ID' => $this->risk_guid,
2024-05-20 03:59:44 +02:00
], $headers);
}
/**
* Generates a client token for the payment form.
*
* @return string
*/
public function getClientToken(): string
{
$r = $this->gatewayRequest('/v1/identity/generate-token', 'post', ['body' => '']);
if($r->successful()) {
return $r->json()['client_token'];
}
throw new PaymentFailed('Unable to gain client token from Paypal. Check your configuration', 401);
}
public function auth(): bool
{
try {
$this->init()->getClientToken();
return true;
2024-06-14 09:09:44 +02:00
} catch(\Exception $e) {
2024-05-20 03:59:44 +02:00
}
return false;
}
public function importCustomers()
{
return true;
2024-06-14 09:09:44 +02:00
}
2024-05-20 03:59:44 +02:00
public function processWebhookRequest(Request $request)
{
$this->init();
PayPalWebhook::dispatch($request->all(), $request->headers->all(), $this->access_token);
2024-07-21 03:12:07 +02:00
2024-05-20 03:59:44 +02:00
}
2024-06-14 09:09:44 +02:00
2024-06-14 09:32:07 +02:00
public function createNinjaPayment($request, $response)
{
2024-07-20 05:36:38 +02:00
if(isset($response['purchase_units'][0]['payments']['captures'][0]['status']) && in_array($response['purchase_units'][0]['payments']['captures'][0]['status'], ['COMPLETED', 'PENDING']))
{
$payment_status = $response['purchase_units'][0]['payments']['captures'][0]['status'] == 'COMPLETED' ? \App\Models\Payment::STATUS_COMPLETED : \App\Models\Payment::STATUS_PENDING;
$data = [
'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,
];
2024-06-14 09:32:07 +02:00
2024-07-20 05:36:38 +02:00
$payment = $this->createPayment($data, $payment_status);
2024-06-14 09:32:07 +02:00
2024-07-20 05:36:38 +02:00
if ($request->has('store_card') && $request->input('store_card') === true) {
$payment_source = $response->json()['payment_source'] ?? false;
2024-06-14 09:32:07 +02:00
2024-07-20 05:36:38 +02:00
if(isset($payment_source['card']) && ($payment_source['card']['attributes']['vault']['status'] ?? false) && $payment_source['card']['attributes']['vault']['status'] == 'VAULTED') {
2024-06-14 09:32:07 +02:00
2024-07-20 05:36:38 +02:00
$last4 = $payment_source['card']['last_digits'];
$expiry = $payment_source['card']['expiry']; //'2025-01'
$expiry_meta = explode('-', $expiry);
$brand = $payment_source['card']['brand'];
2024-06-14 09:32:07 +02:00
2024-07-20 05:36:38 +02:00
$payment_meta = new \stdClass();
$payment_meta->exp_month = $expiry_meta[1] ?? '';
$payment_meta->exp_year = $expiry_meta[0] ?? $expiry;
$payment_meta->brand = $brand;
$payment_meta->last4 = $last4;
$payment_meta->type = GatewayType::CREDIT_CARD;
2024-06-14 09:32:07 +02:00
2024-07-20 05:36:38 +02:00
$token = $payment_source['card']['attributes']['vault']['id']; // 09f28652d01257021
$gateway_customer_reference = $payment_source['card']['attributes']['vault']['customer']['id']; //rbTHnLsZqE;
2024-06-14 09:32:07 +02:00
2024-07-20 05:36:38 +02:00
$data['token'] = $token;
$data['payment_method_id'] = GatewayType::PAYPAL_ADVANCED_CARDS;
$data['payment_meta'] = $payment_meta;
2024-06-14 09:32:07 +02:00
2024-07-20 05:36:38 +02:00
$additional['gateway_customer_reference'] = $gateway_customer_reference;
2024-06-14 09:32:07 +02:00
2024-07-20 05:36:38 +02:00
$this->storeGatewayToken($data, $additional);
2024-06-14 09:32:07 +02:00
2024-07-20 05:36:38 +02:00
}
2024-06-14 09:32:07 +02:00
}
2024-07-20 05:36:38 +02:00
SystemLogger::dispatch(
['response' => $response->json(), 'data' => $data],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_SUCCESS,
SystemLog::TYPE_PAYPAL,
$this->client,
$this->client->company,
);
2024-06-14 09:32:07 +02:00
2024-07-20 05:36:38 +02:00
return response()->json(['redirect' => route('client.payments.show', ['payment' => $this->encodePrimaryKey($payment->id)], false)]);
}
SystemLogger::dispatch($response, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_PAYPAL, $this->client, $this->client->company);
$error = isset($response['purchase_units'][0]['payments']['captures'][0]['status_details'][0]) ? $response['purchase_units'][0]['payments']['captures'][0]['status_details'][0] : $response['purchase_units'][0]['payments']['captures'][0]['status'];
return response()->json(['message' => $error], 400);
2024-06-14 09:32:07 +02:00
}
2024-06-14 09:09:44 +02:00
}