From a1fa52dfd32084e3f55048c3730d0ccd2c15ab70 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 19 May 2024 19:14:46 +1000 Subject: [PATCH 01/14] Updates for BTC payment driver --- app/Http/Controllers/BaseController.php | 7 ++-- app/Models/CompanyGateway.php | 1 + app/Models/SystemLog.php | 2 ++ composer.lock | 20 +++++------ .../2024_05_03_145535_btcpay_gateway.php | 36 ++++++++++--------- 5 files changed, 36 insertions(+), 30 deletions(-) diff --git a/app/Http/Controllers/BaseController.php b/app/Http/Controllers/BaseController.php index d3b1d56c6d..079030b166 100644 --- a/app/Http/Controllers/BaseController.php +++ b/app/Http/Controllers/BaseController.php @@ -32,7 +32,6 @@ use App\Models\BankIntegration; use App\Models\BankTransaction; use App\Models\ExpenseCategory; use League\Fractal\Resource\Item; -use App\DataMapper\EDoc\Schema\RO; use App\Models\BankTransactionRule; use Illuminate\Support\Facades\Auth; use App\Transformers\ArraySerializer; @@ -42,6 +41,7 @@ use Illuminate\Database\Eloquent\Builder; use League\Fractal\Serializer\JsonApiSerializer; use League\Fractal\Pagination\IlluminatePaginatorAdapter; use Illuminate\Contracts\Container\BindingResolutionException; +use Invoiceninja\Einvoice\Decoder\Schema; /** * Class BaseController. @@ -890,7 +890,6 @@ class BaseController extends Controller /** @phpstan-ignore-next-line **/ $query = $paginator->getCollection();// @phpstan-ignore-line - $resource = new Collection($query, $transformer, $this->entity_type); $resource->setPaginator(new IlluminatePaginatorAdapter($paginator)); } @@ -998,8 +997,8 @@ class BaseController extends Controller if(request()->has('einvoice')){ - $ro = new RO(); - $response_data['einvoice_schema'] = $ro(); + $ro = new Schema(); + $response_data['einvoice_schema'] = $ro('FACT1'); } diff --git a/app/Models/CompanyGateway.php b/app/Models/CompanyGateway.php index 530c0bb461..44f3c606ea 100644 --- a/app/Models/CompanyGateway.php +++ b/app/Models/CompanyGateway.php @@ -155,6 +155,7 @@ class CompanyGateway extends BaseModel 'b9886f9257f0c6ee7c302f1c74475f6c' => 321, //GoCardless 'hxd6gwg3ekb9tb3v9lptgx1mqyg69zu9' => 322, '80af24a6a691230bbec33e930ab40666' => 323, + 'vpyfbmdrkqcicpkjqdusgjfluebftuva' => 324, //BTPay ]; protected $touches = []; diff --git a/app/Models/SystemLog.php b/app/Models/SystemLog.php index 344c8d832c..5ef3305d52 100644 --- a/app/Models/SystemLog.php +++ b/app/Models/SystemLog.php @@ -150,6 +150,8 @@ class SystemLog extends Model public const TYPE_PAYPAL_PPCP = 323; + public const TYPE_BTC_PAY = 324; + public const TYPE_QUOTA_EXCEEDED = 400; public const TYPE_UPSTREAM_FAILURE = 401; diff --git a/composer.lock b/composer.lock index a9fb7f986e..e2d05231eb 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "da485c7cec773404ffe59d450b6505cf", + "content-hash": "1356155e46e797b140685c105df97b8e", "packages": [ { "name": "adrienrn/php-mimetyper", @@ -3221,26 +3221,26 @@ }, { "name": "firebase/php-jwt", - "version": "v6.10.0", + "version": "v6.10.1", "source": { "type": "git", "url": "https://github.com/firebase/php-jwt.git", - "reference": "a49db6f0a5033aef5143295342f1c95521b075ff" + "reference": "500501c2ce893c824c801da135d02661199f60c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/firebase/php-jwt/zipball/a49db6f0a5033aef5143295342f1c95521b075ff", - "reference": "a49db6f0a5033aef5143295342f1c95521b075ff", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/500501c2ce893c824c801da135d02661199f60c5", + "reference": "500501c2ce893c824c801da135d02661199f60c5", "shasum": "" }, "require": { - "php": "^7.4||^8.0" + "php": "^8.0" }, "require-dev": { - "guzzlehttp/guzzle": "^6.5||^7.4", + "guzzlehttp/guzzle": "^7.4", "phpspec/prophecy-phpunit": "^2.0", "phpunit/phpunit": "^9.5", - "psr/cache": "^1.0||^2.0", + "psr/cache": "^2.0||^3.0", "psr/http-client": "^1.0", "psr/http-factory": "^1.0" }, @@ -3278,9 +3278,9 @@ ], "support": { "issues": "https://github.com/firebase/php-jwt/issues", - "source": "https://github.com/firebase/php-jwt/tree/v6.10.0" + "source": "https://github.com/firebase/php-jwt/tree/v6.10.1" }, - "time": "2023-12-01T16:26:39+00:00" + "time": "2024-05-18T18:05:11+00:00" }, { "name": "fruitcake/php-cors", diff --git a/database/migrations/2024_05_03_145535_btcpay_gateway.php b/database/migrations/2024_05_03_145535_btcpay_gateway.php index 5f952ce4ff..b618db1f56 100644 --- a/database/migrations/2024_05_03_145535_btcpay_gateway.php +++ b/database/migrations/2024_05_03_145535_btcpay_gateway.php @@ -12,24 +12,28 @@ return new class extends Migration */ public function up(): void { - $gateway = new Gateway; - $gateway->name = 'BTCPay'; - $gateway->key = 'vpyfbmdrkqcicpkjqdusgjfluebftuva'; - $gateway->provider = 'BTCPay'; - $gateway->is_offsite = true; + if(!Gateway::find(62)) + { + $gateway = new Gateway; + $gateway->id = 62; + $gateway->name = 'BTCPay'; + $gateway->key = 'vpyfbmdrkqcicpkjqdusgjfluebftuva'; + $gateway->provider = 'BTCPay'; + $gateway->is_offsite = true; - $btcpayFieds = new \stdClass; - $btcpayFieds->btcpayUrl = ""; - $btcpayFieds->apiKey = ""; - $btcpayFieds->storeId = ""; - $btcpayFieds->webhookSecret = ""; - $gateway->fields = \json_encode($btcpayFieds); + $btcpayFieds = new \stdClass; + $btcpayFieds->btcpayUrl = ""; + $btcpayFieds->apiKey = ""; + $btcpayFieds->storeId = ""; + $btcpayFieds->webhookSecret = ""; + $gateway->fields = \json_encode($btcpayFieds); - $gateway->visible = true; - $gateway->site_url = 'https://btcpayserver.org'; - $gateway->default_gateway_type_id = GatewayType::CRYPTO; - $gateway->save(); + $gateway->visible = true; + $gateway->site_url = 'https://btcpayserver.org'; + $gateway->default_gateway_type_id = GatewayType::CRYPTO; + $gateway->save(); + } } /** @@ -39,4 +43,4 @@ return new class extends Migration { // } -}; +}; \ No newline at end of file From aaae12e6910e84c13c08fa54ba29e6324ea28d10 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 19 May 2024 19:42:20 +1000 Subject: [PATCH 02/14] Capture additional metrics --- app/DataMapper/Analytics/DbQuery.php | 4 ++++ app/Http/Middleware/QueryLogging.php | 17 ++++++++++++++++- database/factories/AccountFactory.php | 1 + 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/app/DataMapper/Analytics/DbQuery.php b/app/DataMapper/Analytics/DbQuery.php index c5718278da..ac65c29b36 100644 --- a/app/DataMapper/Analytics/DbQuery.php +++ b/app/DataMapper/Analytics/DbQuery.php @@ -52,6 +52,10 @@ class DbQuery extends GenericMixedMetric public $string_metric7 = 'ip_address'; + public $string_metric8 = 'client_version'; + + public $string_metric9 = 'platform'; + /** * The counter * set to 1. diff --git a/app/Http/Middleware/QueryLogging.php b/app/Http/Middleware/QueryLogging.php index 106d498cb5..8f1b8b546e 100644 --- a/app/Http/Middleware/QueryLogging.php +++ b/app/Http/Middleware/QueryLogging.php @@ -73,7 +73,22 @@ class QueryLogging $ip = $request->ip(); } - LightLogs::create(new DbQuery($request->method(), substr(urldecode($request->url()), 0, 180), $count, $time, $ip)) + $client_version = ''; + $platform = ''; + + if ($request->hasHeader('X-CLIENT-PLATFORM')) { + $platform = $request->header('X-CLIENT-PLATFORM'); + } + elseif($request->hasHeader('X-React')){ + $platform = 'react'; + } + + if ($request->hasHeader('X-CLIENT-VERSION')) + { + $client_version = $request->header('X-CLIENT-VERSION'); + } + + LightLogs::create(new DbQuery($request->method(), substr(urldecode($request->url()), 0, 180), $count, $time, $ip, $client_version, $platform)) ->batch(); } diff --git a/database/factories/AccountFactory.php b/database/factories/AccountFactory.php index 371c2a3f91..17cb13d7e8 100644 --- a/database/factories/AccountFactory.php +++ b/database/factories/AccountFactory.php @@ -27,6 +27,7 @@ class AccountFactory extends Factory 'default_company_id' => 1, 'key' => Str::random(32), 'report_errors' => 1, + 'referral_code' => Str::lower(Str::random(32)), ]; } } From fb9c3aca59bf101c3ccefcff9b96b387a4fc1761 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 19 May 2024 19:43:43 +1000 Subject: [PATCH 03/14] Capture additional metrics --- app/Http/Middleware/QueryLogging.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Middleware/QueryLogging.php b/app/Http/Middleware/QueryLogging.php index 8f1b8b546e..efe0ef7077 100644 --- a/app/Http/Middleware/QueryLogging.php +++ b/app/Http/Middleware/QueryLogging.php @@ -73,7 +73,7 @@ class QueryLogging $ip = $request->ip(); } - $client_version = ''; + $client_version = $request->server('HTTP_USER_AGENT'); $platform = ''; if ($request->hasHeader('X-CLIENT-PLATFORM')) { From e8907beeabc402239627e7c51fce9a9e4d5235fa Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 20 May 2024 07:50:39 +1000 Subject: [PATCH 04/14] Updates for translations --- .../PayPalRestPaymentDriver.php | 4 +- lang/en/texts.php | 5 + lang/fr_CA/texts.php | 20 ++ lang/ja/texts.php | 24 ++- .../js/clients/payments/square-credit-card.js | 7 +- tests/Integration/Einvoice/FACT1Test.php | 199 ++++++++++++++---- .../Einvoice/samples/fact1_no_prefixes.xml | 113 ++++++++++ 7 files changed, 325 insertions(+), 47 deletions(-) create mode 100644 tests/Integration/Einvoice/samples/fact1_no_prefixes.xml diff --git a/app/PaymentDrivers/PayPalRestPaymentDriver.php b/app/PaymentDrivers/PayPalRestPaymentDriver.php index 5745ab86d7..3768958065 100644 --- a/app/PaymentDrivers/PayPalRestPaymentDriver.php +++ b/app/PaymentDrivers/PayPalRestPaymentDriver.php @@ -170,9 +170,9 @@ class PayPalRestPaymentDriver extends BaseDriver $data['currency'] = $this->client->currency()->code; -// return render('gateways.paypal.ppcp.card', $data); +return render('gateways.paypal.ppcp.card', $data); -return render('gateways.paypal.pay', $data); +// return render('gateways.paypal.pay', $data); } diff --git a/lang/en/texts.php b/lang/en/texts.php index 63654d4849..ed11493f87 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -5320,6 +5320,11 @@ $lang = array( 'task_round_to_nearest' => 'Round To Nearest', 'bulk_updated' => 'Successfully updated data', 'bulk_update' => 'Bulk Update', + 'calculate' => 'Calculate', + 'sum' => 'Sum', + 'money' => 'Money', + 'web_app' => 'Web App', + 'desktop_app' => 'Desktop App', ); return $lang; diff --git a/lang/fr_CA/texts.php b/lang/fr_CA/texts.php index adecee4bb1..ee826a75d2 100644 --- a/lang/fr_CA/texts.php +++ b/lang/fr_CA/texts.php @@ -5297,6 +5297,26 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette 'local_domain_help' => 'Domaine EHLO (facultatif)', 'port_help' => 'ex. 25,587,465', 'host_help' => 'ex. smtp.gmail.com', + 'always_show_required_fields' => 'Permet l\'affichage des champs requis d\'un formulaire', + 'always_show_required_fields_help' => 'Affiche toujours les champs requis d\'un formulaire au paiement', + 'advanced_cards' => 'Cartes avancées', + 'activity_140' => 'État de compte envoyé à :client', + 'invoice_net_amount' => 'Montant net de la facture', + 'round_to_minutes' => 'Arrondir aux minutes', + '1_minute' => '1 minute', + '5_minutes' => '5 minutes', + '15_minutes' => '15 minutes', + '30_minutes' => '30 minutes', + '1_hour' => '1 heure', + '1_day' => '1 jour', + 'round_tasks' => 'Arrondir les tâches', + 'round_tasks_help' => 'Arrondir les intervales à la sauvegarde des tâches', + 'direction' => 'Direction', + 'round_up' => 'Arrondir à hausse', + 'round_down' => 'Arrondir à la baisse', + 'task_round_to_nearest' => 'Arrondir au plus près', + 'bulk_updated' => 'Les données ont été mises à jour', + 'bulk_update' => 'Mise à jour groupée', ); return $lang; diff --git a/lang/ja/texts.php b/lang/ja/texts.php index 9f046c23ce..dfec27c63c 100644 --- a/lang/ja/texts.php +++ b/lang/ja/texts.php @@ -700,7 +700,7 @@ $lang = array( 'total_invoiced' => 'Total Invoiced', 'open_balance' => 'Open Balance', 'verify_email' => 'Please visit the link in the account confirmation email to verify your email address.', - 'basic_settings' => 'Basic Settings', + 'basic_settings' => '基本設定', 'pro' => 'Pro', 'gateways' => 'ペイメントゲートウェイ', 'next_send_on' => 'Send Next: :date', @@ -1344,7 +1344,7 @@ $lang = array( 'auto_bill_payment_method_credit_card' => 'クレジットカード', 'auto_bill_payment_method_paypal' => 'PayPal アカウント', 'auto_bill_notification_placeholder' => 'この請求書は、期日にて登録されているクレジットカードに自動的に請求されます。', - 'payment_settings' => 'Payment Settings', + 'payment_settings' => '支払い設定', 'on_send_date' => '発送日', 'on_due_date' => '支払期日', @@ -5300,6 +5300,26 @@ $lang = array( 'local_domain_help' => 'EHLO domain (optional)', 'port_help' => 'ie. 25,587,465', 'host_help' => 'ie. smtp.gmail.com', + 'always_show_required_fields' => 'Allows show required fields form', + 'always_show_required_fields_help' => 'Displays the required fields form always at checkout', + 'advanced_cards' => 'Advanced Cards', + 'activity_140' => 'Statement sent to :client', + 'invoice_net_amount' => 'Invoice Net Amount', + 'round_to_minutes' => 'Round To Minutes', + '1_minute' => '1 Minute', + '5_minutes' => '5 Minutes', + '15_minutes' => '15 Minutes', + '30_minutes' => '30 Minutes', + '1_hour' => '1 Hour', + '1_day' => '1 Day', + 'round_tasks' => 'Round Tasks', + 'round_tasks_help' => 'Round time intervals when saving tasks', + 'direction' => 'Direction', + 'round_up' => 'Round Up', + 'round_down' => 'Round Down', + 'task_round_to_nearest' => 'Round To Nearest', + 'bulk_updated' => 'Successfully updated data', + 'bulk_update' => 'Bulk Update', ); return $lang; diff --git a/resources/js/clients/payments/square-credit-card.js b/resources/js/clients/payments/square-credit-card.js index ca8a153e93..4242eae5d6 100644 --- a/resources/js/clients/payments/square-credit-card.js +++ b/resources/js/clients/payments/square-credit-card.js @@ -171,15 +171,10 @@ class SquareCreditCard { document.querySelector('input[name=token]').value = ''; }); - // let toggleWithToken = document.querySelector( - // '.toggle-payment-with-token' - // ); - - // if (!toggleWithToken) { document.getElementById('loader').classList.add('hidden'); document.getElementById('payment-list').classList.remove('hidden'); document.getElementById('toggle-payment-with-credit-card')?.click(); - // } + }); } diff --git a/tests/Integration/Einvoice/FACT1Test.php b/tests/Integration/Einvoice/FACT1Test.php index 11433b9bea..09364f5fbd 100644 --- a/tests/Integration/Einvoice/FACT1Test.php +++ b/tests/Integration/Einvoice/FACT1Test.php @@ -12,6 +12,8 @@ namespace Tests\Integration\Einvoice; use Tests\TestCase; +use Sabre\Xml\Reader; +use Sabre\Xml\Service; use Invoiceninja\Einvoice\Models\FACT1\Invoice; /** @@ -20,54 +22,177 @@ use Invoiceninja\Einvoice\Models\FACT1\Invoice; class FACT1Test extends TestCase { + public array $set = []; protected function setUp(): void { parent::setUp(); } - public function testValidationFact1() + +// public function testValidationFact1() +// { + +// $files = [ +// 'tests/Integration/Einvoice/samples/fact1.xml', +// ]; + +// foreach($files as $f) { + +// $xml = file_get_contents($f); + +// // Remove namespaces and prefixes +// $xmlWithoutNamespacesAndPrefixes = $this->removeNamespacesAndPrefixes($xml); + +// // Output the result +// nlog($xmlWithoutNamespacesAndPrefixes); + + + +// } + + + public function removeNamespacesFromArray($data) { - - $files = [ - 'tests/Integration/Einvoice/samples/fact1.xml', - ]; - - foreach($files as $f) { - - $xmlstring = file_get_contents($f); - -// nlog($xmlstring); - - $xml = simplexml_load_string($xmlstring, "SimpleXMLElement"); - $json = json_encode($xml); - $payload = json_decode($json, true); - - nlog($xml); - nlog($payload); - $validation_array = false; - try { - $rules = Invoice::getValidationRules($payload); - nlog($rules); - - $this->assertIsArray($rules); - - $payload = Invoice::from($payload)->toArray(); - nlog($payload); - $this->assertIsArray($payload); - - $validation_array = Invoice::validate($payload); - - $this->assertIsArray($validation_array); - - } catch(\Illuminate\Validation\ValidationException $e) { - - nlog($e->errors()); + if (is_array($data)) { + foreach ($data as &$item) { + if (isset($item['name'])) { + // Remove the namespace from the name + $item['name'] = preg_replace('/^\{\}(.+)/', '$1', $item['name']); + } + if (isset($item['value']) && is_array($item['value'])) { + // Recursively process child elements + $item['value'] = $this->removeNamespacesFromArray($item['value']); + } + if (isset($item['attributes'])) { + unset($item['attributes']); + // Process attributes if needed + // $item['attributes'] = array_map(function ($attr) { + // return preg_replace('/^\{\}(.+)/', '$1', $attr); + // }, $item['attributes']); + } } + } + return $data; + } - $this->assertIsArray($validation_array); + +function convertToKeyValue($data) +{ + $result = []; + foreach ($data as $item) { + // Remove namespace prefix if present + $name = preg_replace('/^\{\}(.+)/', '$1', $item['name']); + $result[$name] = $item['value']; + } + return $result; +} + + +public function keyValueDeserializer(Reader $reader) +{ + $values = []; + $reader->read(); + $reader->next(); + foreach ($reader->parseGetElements() as $element) { + // Strip the namespace prefix + echo "merp".PHP_EOL; + $name = preg_replace('/^\{\}.*/', '', $element['name']); + $values[$name] = $element['value']; + } + return $values; +} + + + public function testFactToArray() + { + + $xml = file_get_contents('tests/Integration/Einvoice/samples/fact1_no_prefixes.xml'); + $service = new Service(); + + // $service->elementMap = [ + // '{}' => 'Sabre\Xml\Deserializer\keyValue', + // ]; + + // $service->elementMap = [ + // '{}*' => function (Reader $reader) use ($service) { + // return $this->keyValueDeserializer($reader); + // } + // ]; + + + $result = $this->removeNamespacesFromArray($service->parse($xml)); + + + // Convert parsed XML to key-value array + if (isset($result['value']) && is_array($result['value'])) { + $keyValueArray = $this->convertToKeyValue($result['value']); + } else { + $keyValueArray = []; } + // Output the result + nlog($keyValueArray); + + + // nlog($cleanedArray); + nlog($service->parse($xml)); + } + + // Output the result + // ($xmlWithoutNamespaces); + + // $reader = new Reader(); + // $service = new Service(); + + // $service->elementMap = [ + // '*' => 'Sabre\Xml\Deserializer\keyValue', + // ]; + + // nlog($service->parse($xmlstring)); + + // $payload =''; + + // // $reader->xml($xmlstring); + // // $payload = $reader->parse(); + + // // nlog($payload); + // $validation_array = false; + // try { + // $rules = Invoice::getValidationRules($payload); + // nlog($rules); + + // $this->assertIsArray($rules); + + // $payload = Invoice::from($payload)->toArray(); + // nlog($payload); + // $this->assertIsArray($payload); + + // $validation_array = Invoice::validate($payload); + + // $this->assertIsArray($validation_array); + + // } catch(\Illuminate\Validation\ValidationException $e) { + + // nlog($e->errors()); + // } + + // $this->assertIsArray($validation_array); + + + // } + + private function extractName($name): string + { + + $pattern = '/\{[^{}]*\}([^{}]*)/'; + + if (preg_match($pattern, $name, $matches)) { + $extracted = $matches[1]; + return $extracted; + } + + return $name; } } \ No newline at end of file diff --git a/tests/Integration/Einvoice/samples/fact1_no_prefixes.xml b/tests/Integration/Einvoice/samples/fact1_no_prefixes.xml new file mode 100644 index 0000000000..137b1e1912 --- /dev/null +++ b/tests/Integration/Einvoice/samples/fact1_no_prefixes.xml @@ -0,0 +1,113 @@ + + + 2.1 + urn:cen.eu:en16931:2017#compliant#urn:efactura.mfinante.ro:CIUS-RO:1.0.1 + ABC 0020 + 2024-01-01 + 2024-01-15 + 384 + RON + RON + + + + 234234234 + + + This can be the full address , not just the street and street nr. + SECTOR2 + RO-B + + RO + + + + RO234234234 + + VAT + + + + Some Copany Name + J40/2222/2009 + + + Someone + 88282819832 + some@email.com + + + + + + + 646546549 + + + This can be the full address , not just the street and street nr. + SECTOR3 + RO-B + + RO + + + + Some Comapny + 646546549 + + + Someone + + some@email.com + + + + + 42 + + some account nr + Bank name + + + + 63.65 + + 335.00 + 63.65 + + S // this is a speciffic identifier for the VAT type + 19 + + VAT + + + + + + 335.00 + 335.00 + 398.65 + 0.00 + 398.65 + + + 1 + 1 // unitcode is a speciffic + identifier for the type of product + 335.00 + + Some Description + Some product + + S // this is a speciffic identifier for the VAT type + 19 + + VAT + + + + + 335 + + + \ No newline at end of file From 91078eb6a1637567d9d1967a08a6d20d5be67017 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 20 May 2024 08:31:28 +1000 Subject: [PATCH 05/14] Additional props for account/companies table --- app/Models/Account.php | 1 + app/Models/Company.php | 1 + app/PaymentDrivers/AuthorizePaymentDriver.php | 2 +- app/Transformers/CompanyTransformer.php | 1 + ..._19_215103_2024_05_20_einvoice_columns.php | 33 +++++++++++++++++++ 5 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 database/migrations/2024_05_19_215103_2024_05_20_einvoice_columns.php diff --git a/app/Models/Account.php b/app/Models/Account.php index c2b54f2521..81d262454b 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -31,6 +31,7 @@ use Laracasts\Presenter\PresentableTrait; * App\Models\Account * * @property int $id + * @property int $email_quota * @property string|null $plan * @property string|null $plan_term * @property string|null $plan_started diff --git a/app/Models/Company.php b/app/Models/Company.php index 0538ddddf5..bc2c36f75d 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -388,6 +388,7 @@ class Company extends BaseModel 'e_invoice_certificate_passphrase' => EncryptedCast::class, 'smtp_username' => 'encrypted', 'smtp_password' => 'encrypted', + 'einvoice' => 'object', ]; protected $with = []; diff --git a/app/PaymentDrivers/AuthorizePaymentDriver.php b/app/PaymentDrivers/AuthorizePaymentDriver.php index bd1ee2274e..465474a5d5 100644 --- a/app/PaymentDrivers/AuthorizePaymentDriver.php +++ b/app/PaymentDrivers/AuthorizePaymentDriver.php @@ -67,7 +67,7 @@ class AuthorizePaymentDriver extends BaseDriver public function getClientRequiredFields(): array { $data = [ - ['name' => 'client_name', 'label' => ctrans('texts.name'), 'type' => 'text', 'validation' => 'required|min:2'], + // ['name' => 'client_name', 'label' => ctrans('texts.name'), 'type' => 'text', 'validation' => 'required|min:2'], ['name' => 'client_phone', 'label' => ctrans('texts.phone'), 'type' => 'text', 'validation' => 'required'], ['name' => 'contact_email', 'label' => ctrans('texts.email'), 'type' => 'text', 'validation' => 'required|email:rfc'], ['name' => 'client_address_line_1', 'label' => ctrans('texts.address1'), 'type' => 'text', 'validation' => 'required'], diff --git a/app/Transformers/CompanyTransformer.php b/app/Transformers/CompanyTransformer.php index 7c94978d72..08b117288c 100644 --- a/app/Transformers/CompanyTransformer.php +++ b/app/Transformers/CompanyTransformer.php @@ -211,6 +211,7 @@ class CompanyTransformer extends EntityTransformer 'smtp_password' => $company->smtp_password ? '********' : '', 'smtp_local_domain' => (string)$company->smtp_local_domain ?? '', 'smtp_verify_peer' => (bool)$company->smtp_verify_peer, + 'einvoice' => $company->einvoice ?: new \stdClass(), ]; } diff --git a/database/migrations/2024_05_19_215103_2024_05_20_einvoice_columns.php b/database/migrations/2024_05_19_215103_2024_05_20_einvoice_columns.php new file mode 100644 index 0000000000..19f3e0bb11 --- /dev/null +++ b/database/migrations/2024_05_19_215103_2024_05_20_einvoice_columns.php @@ -0,0 +1,33 @@ +mediumText('einvoice')->nullable(); + }); + + + Schema::table('accounts', function (Blueprint $table) { + $table->integer('email_quota')->default(20)->nullable(); + }); + + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // + } +}; From 8dde7024fc0d9e64f6e890f253cd3102a3cdba48 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 20 May 2024 11:59:44 +1000 Subject: [PATCH 06/14] extend paypal classes --- .../PayPal/PayPalBasePaymentDriver.php | 427 +++++++++++++++ .../PayPalPPCPPaymentDriver.php | 412 +++----------- .../PayPalRestPaymentDriver.php | 505 +++--------------- .../gateways/paypal/ppcp/card.blade.php | 24 +- 4 files changed, 585 insertions(+), 783 deletions(-) create mode 100644 app/PaymentDrivers/PayPal/PayPalBasePaymentDriver.php diff --git a/app/PaymentDrivers/PayPal/PayPalBasePaymentDriver.php b/app/PaymentDrivers/PayPal/PayPalBasePaymentDriver.php new file mode 100644 index 0000000000..8eb75b0434 --- /dev/null +++ b/app/PaymentDrivers/PayPal/PayPalBasePaymentDriver.php @@ -0,0 +1,427 @@ + '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() + { + + $funding_options = + + 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. */ + if(in_array(1, $funding_options) && in_array(29, $funding_options)){ + + if (($key = array_search(1, $funding_options)) !== false) + unset($funding_options[$key]); + + } + + 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, + }; + + return $method; + } + + public function init() + { + + $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; + + } + + + /** + * getFundingOptions + * + * Hosted fields requires this. + * + * @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, ','); + + } + + public 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(), + ], + ] + + : [ + "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, + ]; + } + + 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 + "usage"=> "DERIVED", + ], + ], + ]; + + } + + $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(), + ]; + } + elseif( + 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 + ) + { + $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; + + } + + /** + * 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); + + if($r->successful()) { + return $r; + } + + 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, + ); + + throw new PaymentFailed("Gateway failure - {$r->body()}", 401); + + } + + /** + * Generates the request headers + * + * @param array $headers + * @return array + */ + public function getHeaders(array $headers = []): array + { + 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(), + ], $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; + } + catch(\Exception $e) { + + } + + return false; + } + + public function importCustomers() + { + return true; + } + + public function processWebhookRequest(Request $request) + { + + // nlog(json_encode($request->all())); + $this->init(); + + PayPalWebhook::dispatch($request->all(), $request->headers->all(), $this->access_token); + } + +} \ No newline at end of file diff --git a/app/PaymentDrivers/PayPalPPCPPaymentDriver.php b/app/PaymentDrivers/PayPalPPCPPaymentDriver.php index 12758fa101..0c099689e5 100644 --- a/app/PaymentDrivers/PayPalPPCPPaymentDriver.php +++ b/app/PaymentDrivers/PayPalPPCPPaymentDriver.php @@ -21,188 +21,34 @@ use Illuminate\Http\Request; use App\Jobs\Util\SystemLogger; use App\Utils\Traits\MakesHash; use App\Exceptions\PaymentFailed; +use App\Models\ClientGatewayToken; use Illuminate\Support\Facades\Http; use App\PaymentDrivers\PayPal\PayPalWebhook; +use App\PaymentDrivers\PayPal\PayPalBasePaymentDriver; -class PayPalPPCPPaymentDriver extends BaseDriver +class PayPalPPCPPaymentDriver extends PayPalBasePaymentDriver { use MakesHash; - - public $token_billing = false; - - public $can_authorise_credit_card = false; - - private $omnipay_gateway; - - private float $fee = 0; + +///v1/customer/partners/merchant-accounts/{merchant_id}/capabilities - test if advanced cards is available. +// { +// "capabilities": [ +// { +// "name": "ADVANCED_CARD_PAYMENTS", +// "status": "ENABLED" +// }, +// { +// "name": "VAULTING", +// "status": "ENABLED" +// } +// ] +// } public const SYSTEM_LOG_TYPE = SystemLog::TYPE_PAYPAL_PPCP; - - private string $api_endpoint_url = ''; - - 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', - 25 => 'venmo', - // 9 => 'sepa', - // 12 => 'bancontact', - // 17 => 'eps', - // 15 => 'giropay', - // 13 => 'ideal', - // 26 => 'mercadopago', - // 27 => 'mybank', - 28 => 'paylater', - // 16 => 'p24', - // 7 => 'sofort' - ]; - + /** - * Return an array of - * enabled gateway payment methods - * - * @return array - */ - public function gatewayTypes(): array - { - - return collect($this->company_gateway->fees_and_limits) - ->filter(function ($fee) { - return $fee->is_enabled; - })->map(function ($fee, $key) { - return (int)$key; - })->toArray(); - - } - - 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; - } - - private 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, ','); - - } - - /** - * Initialize the Paypal gateway. - * - * Attempt to generate and return the access token. - * - * @return self - */ - public function init(): self - { - - $this->api_endpoint_url = 'https://api-m.paypal.com'; - // $this->api_endpoint_url = 'https://api-m.sandbox.paypal.com'; - $secret = config('ninja.paypal.secret'); - $client_id = config('ninja.paypal.client_id'); - - 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; - - } - - /** - * 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; - } - - /** - * Checks whether payments are enabled on the account - * + * Checks whether payments are enabled on the merchant account + * * @return self */ private function checkPaymentsReceivable(): self @@ -252,7 +98,10 @@ class PayPalPPCPPaymentDriver extends BaseDriver $data['merchantId'] = $this->company_gateway->getConfigField('merchantId'); $data['currency'] = $this->client->currency()->code; - return render('gateways.paypal.ppcp.pay', $data); + if($this->paypal_payment_method == 29) + return render('gateways.paypal.ppcp.card', $data); + else + return render('gateways.paypal.ppcp.pay', $data); } @@ -372,74 +221,13 @@ class PayPalPPCPPaymentDriver extends BaseDriver return $r->json(); } - /** - * Generates a client token for the payment form. - * - * @return string - */ - private 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); - - } - - /** - * Builds the payment request. - * - * @return array - */ - private function paymentSource(): array - { - /** we only need to support paypal as payment source until as we are only using hosted payment buttons */ - return $this->injectPayPalPaymentSource(); - - } - - private function injectPayPalPaymentSource(): array - { - - $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( - 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 - ) - { - $order["paypal"]["address"] = $this->getBillingAddress(); - } - - return $order; - - } - /** * Creates the PayPal Order object * * @param array $data * @return string */ - private function createOrder(array $data): string + public function createOrder(array $data): string { $_invoice = collect($this->payment_hash->data->invoices)->first(); @@ -453,7 +241,7 @@ class PayPalPPCPPaymentDriver extends BaseDriver $order = [ "intent" => "CAPTURE", - "payment_source" => $this->paymentSource(), + "payment_source" => $this->getPaymentSource(), "purchase_units" => [ [ "custom_id" => $this->payment_hash->hash, @@ -465,7 +253,6 @@ class PayPalPPCPPaymentDriver extends BaseDriver "payment_instruction" => [ "disbursement_mode" => "INSTANT", ], - $this->getShippingAddress(), "amount" => [ "value" => (string)$data['amount_with_fee'], "currency_code" => $this->client->currency()->code, @@ -502,119 +289,66 @@ class PayPalPPCPPaymentDriver extends BaseDriver } - private 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, - ]; - } - - 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 + * processTokenPayment * - * @param string $uri - * @param string $verb - * @param array $data - * @param ?array $headers - * @return \Illuminate\Http\Client\Response + * With PayPal and token payments, the order needs to be + * deleted and then created with the payment source that + * has been selected by the client. + * + * This method handle the deletion of the current paypal order, + * and the automatic payment of the order with the selected payment source. + * + * @param mixed $request + * @param array $response + * @return void */ - public function gatewayRequest(string $uri, string $verb, array $data, ?array $headers = []) - { - $this->init(); + public function processTokenPayment($request, array $response) { - $r = Http::withToken($this->access_token) - ->withHeaders($this->getHeaders($headers)) - ->{$verb}("{$this->api_endpoint_url}{$uri}", $data); + $cgt = ClientGatewayToken::where('client_id', $this->client->id) + ->where('token', $request['token']) + ->firstOrFail(); - if($r->successful()) { - return $r; - } + $orderId = $response['orderID']; + $r = $this->gatewayRequest("/v1/checkout/orders/{$orderId}/", 'delete', ['body' => '']); + + $data['amount_with_fee'] = $this->payment_hash->data->amount_with_fee; + $data["payment_source"] = [ + "card" => [ + "vault_id" => $cgt->token, + "stored_credential" => [ + "payment_initiator" => "MERCHANT", + "payment_type" => "UNSCHEDULED", + "usage" => "SUBSEQUENT", + ], + ], + ]; + + $orderId = $this->createOrder($data); + + $r = $this->gatewayRequest("/v2/checkout/orders/{$orderId}", 'get', ['body' => '']); + + $response = $r->json(); + + $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' => $this->gateway_type_id, + ]; + + $payment = $this->createPayment($data, \App\Models\Payment::STATUS_COMPLETED); SystemLogger::dispatch( - ['response' => $r->body()], + ['response' => $response, 'data' => $data], SystemLog::CATEGORY_GATEWAY_RESPONSE, - SystemLog::EVENT_GATEWAY_FAILURE, + SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::TYPE_PAYPAL, $this->client, $this->client->company, ); - throw new PaymentFailed("Gateway failure - {$r->body()}", 401); + return redirect()->route('client.payments.show', ['payment' => $this->encodePrimaryKey($payment->id)]); } - - /** - * Generates the request headers - * - * @param array $headers - * @return array - */ - private function getHeaders(array $headers = []): array - { - 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(), - ], $headers); - } - - public function processWebhookRequest(Request $request) - { - - // nlog(json_encode($request->all())); - $this->init(); - - PayPalWebhook::dispatch($request->all(), $request->headers->all(), $this->access_token); - } - - public function auth(): bool - { - - try { - $this->init()->getClientToken(); - return true; - } - catch(\Exception $e) { - - } - - return false; - } - - public function importCustomers() - { - - // $response = $this->gatewayRequest('/v1/reporting/transactions', 'get', ['fields' => 'all','page_size' => 500,'start_date' => '2024-02-01T00:00:00-0000', 'end_date' => '2024-03-01T00:00:00-0000']); - - // nlog($response->json()); - - return true; - } } diff --git a/app/PaymentDrivers/PayPalRestPaymentDriver.php b/app/PaymentDrivers/PayPalRestPaymentDriver.php index 3768958065..e250713f0c 100644 --- a/app/PaymentDrivers/PayPalRestPaymentDriver.php +++ b/app/PaymentDrivers/PayPalRestPaymentDriver.php @@ -13,7 +13,6 @@ namespace App\PaymentDrivers; use Carbon\Carbon; -use Omnipay\Omnipay; use App\Models\Invoice; use App\Models\SystemLog; use App\Models\GatewayType; @@ -23,136 +22,15 @@ use App\Jobs\Util\SystemLogger; use App\Utils\Traits\MakesHash; use App\Exceptions\PaymentFailed; use App\Models\ClientGatewayToken; +use App\PaymentDrivers\PayPal\PayPalBasePaymentDriver; use Illuminate\Support\Facades\Http; -class PayPalRestPaymentDriver extends BaseDriver +class PayPalRestPaymentDriver extends PayPalBasePaymentDriver { use MakesHash; - public $token_billing = false; - - public $can_authorise_credit_card = false; - - private $omnipay_gateway; - - private float $fee = 0; - public const SYSTEM_LOG_TYPE = SystemLog::TYPE_PAYPAL; - private string $api_endpoint_url = ''; - - 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', - 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() - { - - $funding_options = []; - - foreach ($this->company_gateway->fees_and_limits as $key => $value) { - if ($value->is_enabled) { - $funding_options[] = $key; - } - } - - return $funding_options; - - } - - public function init() - { - - $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; - - } - - 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, - "29" => $method = PaymentType::CREDIT_CARD_OTHER, - }; - - 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; - } - - 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; - } - public function processPaymentView($data) { $this->init(); @@ -169,110 +47,20 @@ class PayPalRestPaymentDriver extends BaseDriver $data['gateway_type_id'] = $this->gateway_type_id; $data['currency'] = $this->client->currency()->code; - -return render('gateways.paypal.ppcp.card', $data); - -// return render('gateways.paypal.pay', $data); + if($this->paypal_payment_method == 29) + return render('gateways.paypal.ppcp.card', $data); + else + return render('gateways.paypal.pay', $data); } - - private function getFundingOptions(): string - { - - $enums = [ - 3 => 'paypal', - 1 => 'card', - 25 => 'venmo', - // 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, ','); - - } - - public function processTokenPayment($request, array $response) { - - $cgt = ClientGatewayToken::where('client_id', $this->client->id) - ->where('token', $request['token']) - ->firstOrFail(); - nlog("process token"); - - nlog($request->all()); - nlog($response); - - $orderId = $response['orderID']; - $r = $this->gatewayRequest("/v1/checkout/orders/{$orderId}/", 'delete', ['body' => '']); - - nlog($r); - - $data['amount_with_fee'] = $this->payment_hash->data->amount_with_fee; - $data["payment_source"] = [ - "card" => [ - "vault_id" => $cgt->token, - "stored_credential" => [ - "payment_initiator" => "MERCHANT", - "payment_type" => "UNSCHEDULED", - "usage" => "SUBSEQUENT", - // "previous_transaction_reference" => $cgt->gateway_customer_reference, - ], - ], - ]; - - $orderId = $this->createOrder($data); - - nlog("post order creation"); - nlog($orderId); - - - $r = $this->gatewayRequest("/v2/checkout/orders/{$orderId}", 'get', ['body' => '']); - nlog($r); - - $response = $r->json(); - nlog($response); - - $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' => $this->gateway_type_id, - ]; - - $payment = $this->createPayment($data, \App\Models\Payment::STATUS_COMPLETED); - - SystemLogger::dispatch( - ['response' => $response, 'data' => $data], - SystemLog::CATEGORY_GATEWAY_RESPONSE, - SystemLog::EVENT_GATEWAY_SUCCESS, - SystemLog::TYPE_PAYPAL, - $this->client, - $this->client->company, - ); - - return redirect()->route('client.payments.show', ['payment' => $this->encodePrimaryKey($payment->id)]); - - } - + + + /** + * processPaymentResponse + * + * @param mixed $request + * @return void + */ public function processPaymentResponse($request) { @@ -280,12 +68,10 @@ return render('gateways.paypal.ppcp.card', $data); $request['gateway_response'] = str_replace("Error: ", "", $request['gateway_response']); $response = json_decode($request['gateway_response'], true); - nlog($request->all()); if($request->has('token') && strlen($request->input('token')) > 2) return $this->processTokenPayment($request, $response); - // nlog($response); //capture $orderID = $response['orderID']; @@ -367,8 +153,6 @@ return render('gateways.paypal.ppcp.card', $data); private function createNinjaPayment($request, $response) { - nlog($response->json()); - $data = [ 'payment_type' => $this->getPaymentMethod($request->gateway_type_id), 'amount' => $response['purchase_units'][0]['payments']['captures'][0]['amount']['value'], @@ -401,7 +185,6 @@ return render('gateways.paypal.ppcp.card', $data); $data['token'] = $token; $data['payment_method_id'] = GatewayType::PAYPAL_ADVANCED_CARDS; $data['payment_meta'] = $payment_meta; - // $data['payment_method_id'] = GatewayType::CREDIT_CARD; $additional['gateway_customer_reference'] = $gateway_customer_reference; @@ -423,116 +206,7 @@ return render('gateways.paypal.ppcp.card', $data); } - private 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); - - } - - private 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" - ], - // "name" => $this->client->present()->primary_contact_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" - // ], - "stored_credential" => [ - // "payment_initiator" => "MERCHANT", //"CUSTOMER" who initiated the transaction? - "payment_initiator" => "CUSTOMER", //"" who initiated the transaction? - "payment_type" => "UNSCHEDULED", //UNSCHEDULED - "usage"=> "DERIVED", - ], - ], - ]; - - } - - $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(), - ]; - } - elseif( - 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 - ) - { - $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; - - } - - - private function createOrder(array $data): string + public function createOrder(array $data): string { $_invoice = collect($this->payment_hash->data->invoices)->first(); @@ -576,126 +250,79 @@ return render('gateways.paypal.ppcp.card', $data); ] ]; - if($shipping = $this->getShippingAddress()) { $order['purchase_units'][0]["shipping"] = $shipping; } if(isset($data['payment_source'])) $order['payment_source'] = $data['payment_source']; - + $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(), - ], - ] + /** + * processTokenPayment + * + * With PayPal and token payments, the order needs to be + * deleted and then created with the payment source that + * has been selected by the client. + * + * This method handle the deletion of the current paypal order, + * and the automatic payment of the order with the selected payment source. + * + * @param mixed $request + * @param array $response + * @return void + */ + public function processTokenPayment($request, array $response) { - : [ - "name" => [ - "full_name" => $this->client->present()->name() - ] + $cgt = ClientGatewayToken::where('client_id', $this->client->id) + ->where('token', $request['token']) + ->firstOrFail(); + + $orderId = $response['orderID']; + $r = $this->gatewayRequest("/v1/checkout/orders/{$orderId}/", 'delete', ['body' => '']); + + $data['amount_with_fee'] = $this->payment_hash->data->amount_with_fee; + $data["payment_source"] = [ + "card" => [ + "vault_id" => $cgt->token, + "stored_credential" => [ + "payment_initiator" => "MERCHANT", + "payment_type" => "UNSCHEDULED", + "usage" => "SUBSEQUENT", + ], + ], + ]; + + $orderId = $this->createOrder($data); + + $r = $this->gatewayRequest("/v2/checkout/orders/{$orderId}", 'get', ['body' => '']); + + $response = $r->json(); + + $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' => $this->gateway_type_id, ]; - } - - /** - * 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); - - if($r->successful()) { - return $r; - } + $payment = $this->createPayment($data, \App\Models\Payment::STATUS_COMPLETED); SystemLogger::dispatch( - ['response' => $r->body()], + ['response' => $response, 'data' => $data], SystemLog::CATEGORY_GATEWAY_RESPONSE, - SystemLog::EVENT_GATEWAY_FAILURE, + SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::TYPE_PAYPAL, $this->client, - $this->client->company ?? $this->company_gateway->company, + $this->client->company, ); - throw new PaymentFailed("Gateway failure - {$r->body()}", 401); + return redirect()->route('client.payments.show', ['payment' => $this->encodePrimaryKey($payment->id)]); } - - private function getHeaders(array $headers = []): array - { - 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(), - ], $headers); - } - - private function feeCalc($invoice, $invoice_total) - { - $invoice->service()->removeUnpaidGatewayFees(); - $invoice = $invoice->fresh(); - - $balance = floatval($invoice->balance); - - $_updated_invoice = $invoice->service()->addGatewayFee($this->company_gateway, GatewayType::PAYPAL, $invoice_total)->save(); - - if (floatval($_updated_invoice->balance) > $balance) { - $fee = floatval($_updated_invoice->balance) - $balance; - - $this->payment_hash->fee_total = $fee; - $this->payment_hash->save(); - - return $fee; - } - - return 0; - } - - public function auth(): bool - { - - try { - $this->init()->getClientToken(); - return true; - } - catch(\Exception $e) { - - } - - return false; - } - - public function importCustomers() - { - return true; - } - } diff --git a/resources/views/portal/ninja2020/gateways/paypal/ppcp/card.blade.php b/resources/views/portal/ninja2020/gateways/paypal/ppcp/card.blade.php index d285450830..df3a7b50f0 100644 --- a/resources/views/portal/ninja2020/gateways/paypal/ppcp/card.blade.php +++ b/resources/views/portal/ninja2020/gateways/paypal/ppcp/card.blade.php @@ -1,5 +1,18 @@ @extends('portal.ninja2020.layout.payments', ['gateway_title' => ctrans('texts.payment_type_credit_card'), 'card_title' => '']) +@php + $gateway_instance = $gateway instanceof \App\Models\CompanyGateway ? $gateway : $gateway->company_gateway; + $token_billing_string = 'true'; + if($gateway_instance->token_billing == 'off' || $gateway_instance->token_billing == 'optin'){ + $token_billing_string = 'false'; + } + + if (isset($pre_payment) && $pre_payment == '1' && isset($is_recurring) && $is_recurring == '1') { + $token_billing_string = 'true'; + } + + +@endphp @section('gateway_head') @endsection @@ -12,7 +25,7 @@ - + @@ -62,8 +75,12 @@ @push('footer') - +@if(isset($merchantId)) + +@else + +@endif