From 4cbab56de1a3849242c0067309e0b8136617b2b7 Mon Sep 17 00:00:00 2001 From: Mario Lorenz Date: Wed, 7 Aug 2024 11:18:46 +0200 Subject: [PATCH 01/13] collect $shopOrderId after setOrderNumber --- src/Service/Payment.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Service/Payment.php b/src/Service/Payment.php index 223f1ec0..b88eb239 100644 --- a/src/Service/Payment.php +++ b/src/Service/Payment.php @@ -304,6 +304,13 @@ public function doCapturePayPalOrder( $result = $this->fetchOrderFields($checkoutOrderId); } else { $request = new OrderCaptureRequest(); + //order number must be resolved before order patching + $shopOrderId = $order->getFieldData('oxordernr'); + if(!$shopOrderId){ + $order->setOrderNumber(); + $shopOrderId = $order->getFieldData('oxordernr'); + } + try { /** @var ApiOrderModel */ $result = $orderService->capturePaymentForOrder( From d9321e300fd3dc8502372efd326f476e72795f55 Mon Sep 17 00:00:00 2001 From: Mario Lorenz Date: Wed, 7 Aug 2024 11:08:01 +0200 Subject: [PATCH 02/13] add paymentID the actionHash --- src/Core/ServiceFactory.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Core/ServiceFactory.php b/src/Core/ServiceFactory.php index 021da211..2a405625 100644 --- a/src/Core/ServiceFactory.php +++ b/src/Core/ServiceFactory.php @@ -120,6 +120,15 @@ private function getClient(): Client /** @var LoggerInterface $logger */ $logger = $this->getServiceFromContainer('OxidSolutionCatalysts\PayPal\Logger'); + $debug = Registry::getConfig()->getConfigParam('sLogLevel') === 'debug'; + + // prepare a unique action hash + $session = Registry::getSession(); + $sessionId = $session->getId(); + $basketId = $session->getVariable('sess_challenge'); + $paymentId = $session->getVariable('paymentid'); + $actionHash = md5($sessionId . $basketId . $paymentId); + $client = new Client( $logger, $config->isSandbox() ? Client::SANDBOX_URL : Client::PRODUCTION_URL, From 4107835b626bea9dc4f7923e98b4c0f5fce2dc91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Gust?= Date: Tue, 8 Oct 2024 17:54:45 +0200 Subject: [PATCH 03/13] PSPAYPAL-812 remove actionHash since paypal-client in version 3 does not need it --- src/Core/ServiceFactory.php | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/Core/ServiceFactory.php b/src/Core/ServiceFactory.php index 2a405625..021da211 100644 --- a/src/Core/ServiceFactory.php +++ b/src/Core/ServiceFactory.php @@ -120,15 +120,6 @@ private function getClient(): Client /** @var LoggerInterface $logger */ $logger = $this->getServiceFromContainer('OxidSolutionCatalysts\PayPal\Logger'); - $debug = Registry::getConfig()->getConfigParam('sLogLevel') === 'debug'; - - // prepare a unique action hash - $session = Registry::getSession(); - $sessionId = $session->getId(); - $basketId = $session->getVariable('sess_challenge'); - $paymentId = $session->getVariable('paymentid'); - $actionHash = md5($sessionId . $basketId . $paymentId); - $client = new Client( $logger, $config->isSandbox() ? Client::SANDBOX_URL : Client::PRODUCTION_URL, From 7d5bdace048baf58947c48194258abf02cda2f7b Mon Sep 17 00:00:00 2001 From: Mario Lorenz Date: Wed, 7 Aug 2024 11:07:29 +0200 Subject: [PATCH 04/13] provide Action Hash in onBoarding-Client --- src/Controller/Admin/PayPalConfigController.php | 13 ++++++++++++- src/Core/Onboarding/Onboarding.php | 14 ++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/Controller/Admin/PayPalConfigController.php b/src/Controller/Admin/PayPalConfigController.php index 794f158c..c6fa0c3b 100644 --- a/src/Controller/Admin/PayPalConfigController.php +++ b/src/Controller/Admin/PayPalConfigController.php @@ -25,6 +25,7 @@ use OxidSolutionCatalysts\PayPal\Module; use OxidSolutionCatalysts\PayPal\Service\ModuleSettings; use OxidSolutionCatalysts\PayPal\Traits\ServiceContainer; +use OxidSolutionCatalysts\PayPalApi\Exception\ApiException; use Throwable; /** @@ -208,7 +209,17 @@ protected function checkEligibility(): void $onBoardingClient = $handler->getOnboardingClient($config->isSandbox(), true); $merchantInformations = $onBoardingClient->getMerchantInformations(); $handler->saveEligibility($merchantInformations); - } catch (ClientException $exception) { + if (isset($confArr['oscPayPalSetVaulting'])) { + $moduleSettings = $this->getServiceFromContainer(ModuleSettings::class); + $isEligible = $moduleSettings->isSandbox() + ? $moduleSettings->isSandboxVaultingEligibility() + : $moduleSettings->isLiveVaultingEligibility(); + + if (!$isEligible) { + $moduleSettings->save('oscPayPalSetVaulting', false); + } + } + } catch (ClientException|ApiException $exception) { /** @var Logger $logger */ $logger = $this->getServiceFromContainer(Logger::class); diff --git a/src/Core/Onboarding/Onboarding.php b/src/Core/Onboarding/Onboarding.php index cc2860a6..c2be8cfd 100644 --- a/src/Core/Onboarding/Onboarding.php +++ b/src/Core/Onboarding/Onboarding.php @@ -106,6 +106,9 @@ public function getOnboardingClient(bool $isSandbox, bool $withCredentials = fal { $paypalConfig = oxNew(PayPalConfig::class); $partnerConfig = oxNew(PartnerConfig::class); + $session = Registry::getSession(); + $sessionId = $session->getId(); + $actionHash = md5($sessionId); $clientId = ''; $clientSecret = ''; @@ -126,11 +129,18 @@ public function getOnboardingClient(bool $isSandbox, bool $withCredentials = fal $clientSecret, $partnerConfig->getTechnicalPartnerId($isSandbox), $merchantId, - $paypalConfig->getTokenCacheFileName() + $paypalConfig->getTokenCacheFileName(), + $actionHash ); } - public function fetchMerchantInformations() + /** + * @return array + * @throws ApiException + * @throws JsonException + * @throws OnboardingException + */ + public function fetchMerchantInformations(): array { $onboardingResponse = $this->getOnboardingPayload(); /** @var ApiOnboardingClient $apiClient */ From 5d105e99ee2d7172305621d2b06ca532a68c0fcc Mon Sep 17 00:00:00 2001 From: Mario Lorenz Date: Fri, 19 Jul 2024 13:38:21 +0200 Subject: [PATCH 05/13] generate Token only if payPalCustomerId is there --- src/Core/ViewConfig.php | 48 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/Core/ViewConfig.php b/src/Core/ViewConfig.php index be8a1fc3..663bc0c4 100644 --- a/src/Core/ViewConfig.php +++ b/src/Core/ViewConfig.php @@ -248,6 +248,54 @@ protected function getBasePayPalJsSdkUrl($type = '', $continueFlow = false): str return Constants::PAYPAL_JS_SDK_URL . '?' . http_build_query($params); } + public function getUserIdForVaulting(): string + { + if (!$this->getUser()) { + return ""; + } + + $payPalCustomerId = $this->getUser()->getFieldData("oscpaypalcustomerid"); + + if (!$payPalCustomerId) { + return ""; + } + + $vaultingService = Registry::get(ServiceFactory::class)->getVaultingService(); + $response = $vaultingService->generateUserIdToken($payPalCustomerId); + + return $response["id_token"]; + } + + /** + * get Session Vault Success + * + * @return bool|null + */ + public function getSessionVaultSuccess() + { + $session = Registry::getSession(); + $vaultSuccess = $session->getVariable("vaultSuccess"); + $session->deleteVariable("vaultSuccess"); + + return $vaultSuccess; + } + + /** + * get Vault Token + * + * @return string|null + */ + public function getVaultPaymentTokens() + { + if ($this->getIsVaultingActive() && $customerId = $this->getUser()->getFieldData("oscpaypalcustomerid")) { + $vaultingService = Registry::get(ServiceFactory::class)->getVaultingService(); + + return $vaultingService->getVaultPaymentTokens($customerId)["payment_tokens"] ?? null; + } + + return null; + } + public function getDataClientToken(): string { From 503df6cbe599b5c06c516ec8e9305232a8da39cb Mon Sep 17 00:00:00 2001 From: Mario Lorenz Date: Fri, 19 Jul 2024 11:06:05 +0200 Subject: [PATCH 06/13] Set all Request-Methods uppercase because of Method-Check-Restriction --- src/Core/Api/IdentityService.php | 5 +- src/Core/Api/VaultingService.php | 252 +++++++++++++++++++++++++++++ src/Core/Onboarding/Webhook.php | 4 +- src/Core/Tracker/Tracker.php | 2 +- src/Core/Webhook/EventVerifier.php | 2 +- 5 files changed, 258 insertions(+), 7 deletions(-) create mode 100644 src/Core/Api/VaultingService.php diff --git a/src/Core/Api/IdentityService.php b/src/Core/Api/IdentityService.php index 7fbce9d1..c72aa01b 100644 --- a/src/Core/Api/IdentityService.php +++ b/src/Core/Api/IdentityService.php @@ -20,11 +20,10 @@ public function requestClientToken(): array $headers['Content-Type'] = 'application/json'; $headers = array_merge($headers, $this->getAuthHeaders()); - $path = '/generate-token'; - $method = 'post'; + $path = '/v1/identity/generate-token'; /** @var ResponseInterface $response */ - $response = $this->send($method, $path, [], $headers); + $response = $this->send('POST', $path, [], $headers); $body = $response->getBody(); return $body ? json_decode((string)$body, true) : []; diff --git a/src/Core/Api/VaultingService.php b/src/Core/Api/VaultingService.php new file mode 100644 index 00000000..dc20d0f5 --- /dev/null +++ b/src/Core/Api/VaultingService.php @@ -0,0 +1,252 @@ +send('POST', $path, $params, $headers); + $body = $response->getBody(); + + return json_decode((string)$body, true); + } + + /** + * Request a setup token either for card or for PayPal vaulting + * @param bool $card + * @return array + * @throws ApiException + * @throws JsonException + */ + public function createVaultSetupToken(bool $card = false): array + { + if ($card) { + $body = [ + "payment_source" => [ + "card" => [], + ] + ]; + } else { + $body = $this->getPaymentSourceForVaulting($card); + } + + //add customerid if there already is one + if ($paypalCustomerId = Registry::getConfig()->getUser()->getFieldData("oscpaypalcustomerid")) { + $body["customer"] = [ + "id" => $paypalCustomerId + ]; + } + + $headers = $this->getVaultingHeaders(); + + $path = '/v3/vault/setup-tokens'; + + $response = $this->send( + 'POST', + $path, + [], + $headers, + json_encode($body, JSON_THROW_ON_ERROR | JSON_FORCE_OBJECT) + ); + $body = $response->getBody(); + + return json_decode((string)$body, true); + } + + /** + * @param bool $card + * @return array[] + */ + public function getPaymentSourceForVaulting(bool $card): array + { + $viewConf = Registry::get(ViewConfig::class); + $config = Registry::getConfig(); + $user = $viewConf->getUser(); + + $country = oxNew(Country::class); + $country->load($user->getFieldData('oxcountryid')); + + $state = oxNew(State::class); + $state->loadByIdAndCountry( + $user->getFieldData('oxstateid'), + $user->getFieldData('oxcountryid') + ); + + $shopName = Registry::getConfig()->getActiveShop()->getFieldData('oxname'); + $lang = Registry::getLang(); + + $description = sprintf($lang->translateString('OSC_PAYPAL_DESCRIPTION'), $shopName); + + $activeShop = Registry::getConfig()->getActiveShop(); + + $name = $user->getFieldData("oxfname"); + $name .= $user->getFieldData("oxlname"); + $address = [ + "address_line_1" => $user->getFieldData('oxstreet') . " " . $user->getFieldData('oxstreetnr'), + "address_line_2" => $user->getFieldData('oxcompany') . " " . $user->getFieldData('oxaddinfo'), + "admin_area_1" => $state->getFieldData('oxtitle'), + "admin_area_2" => $user->getFieldData('oxcity'), + "postal_code" => $user->getFieldData('oxzip'), + "country_code" => $country->oxcountry__oxisoalpha2->value, + ]; + $locale = + strtolower($country->oxcountry__oxisoalpha2->value) + . '-' + . strtoupper($country->oxcountry__oxisoalpha2->value); + $experience_context = [ + "brand_name" => $activeShop->getFieldData('oxname'), + "locale" => $locale, + "return_url" => $config->getSslShopUrl() . 'index.php?cl=order&fnc=finalizepaypalsession', + "cancel_url" => $config->getSslShopUrl() . 'index.php?cl=order&fnc=cancelpaypalsession', +// "shipping_preference" => "SET_PROVIDED_ADDRESS", + ]; + + if ($card) { + $paymentSource = [ + "card" => [ + "name" => "$name", + "billing_address" => $address, + "verification_method" => "SCA_WHEN_REQUIRED", + "experience_context" => $experience_context, + "attributes" => [ + "verification" => [ + "method" => "SCA_WHEN_REQUIRED" + ], + "vault" => [ + "store_in_vault" => "ON_SUCCESS" + ] + ], + ] + ]; + } else { + $paymentSource = [ + "payment_source" => [ + "paypal" => [ + "description" => $description, + "shipping" => [ + "name" => [ + "full name" => $name + ], + "address" => $address + ], + "usage_type" => "MERCHANT", + "customer_type" => "CONSUMER", + "permit_multiple_payment_tokens" => true, + "usage_pattern" => "IMMEDIATE", + "experience_context" => $experience_context + ] + ] + ]; + } + + return $paymentSource; + } + + public function createVaultPaymentToken($setupToken) + { + $headers = $this->getVaultingHeaders(); + + $path = '/v3/vault/payment-tokens'; + + $requestBody = [ + "payment_source" => [ + "token" => [ + "id" => $setupToken, + "type" => "SETUP_TOKEN", + ] + ] + ]; + + $response = $this->send('POST', $path, [], $headers, json_encode($requestBody)); + $responseBody = $response->getBody(); + + return json_decode((string)$responseBody, true); + } + + public function getVaultPaymentTokens($paypalCustomerId) + { + $viewConf = oxNew(ViewConfig::class); + if (!$viewConf->getIsVaultingActive()) { + return null; + } + + $path = '/v3/vault/payment-tokens?customer_id=' . $paypalCustomerId; + + $response = $this->send('GET', $path); + $body = $response->getBody(); + + return json_decode((string)$body, true); + } + + public function getVaultPaymentTokenByIndex($paypalCustomerId, $index) + { + $paymentTokens = $this->getVaultPaymentTokens($paypalCustomerId); + + return $paymentTokens["payment_tokens"][$index]; + } + + /** + * @param $paymentTokenId + * @return bool + */ + public function deleteVaultedPayment($paymentTokenId) + { + $path = '/v3/vault/payment-tokens/' . $paymentTokenId; + + $response = $this->send('DELETE', $path); + + return $response->getStatusCode() == 204; + } + + /** + * @return array + */ + protected function getVaultingHeaders(): array + { + $headers = []; + $headers['Content-Type'] = 'application/json'; + $headers['PayPal-Partner-Attribution-Id'] = Constants::PAYPAL_PARTNER_ATTRIBUTION_ID_PPCP; + $headers = array_merge($headers, $this->getAuthHeaders()); + return $headers; + } + + protected function getAuthHeaders(): array + { + if (!$this->client->isAuthenticated()) { + $this->client->auth(); + } + + $headers = []; + $headers['Authorization'] = 'Bearer ' . $this->client->getTokenResponse(); + + return $headers; + } +} diff --git a/src/Core/Onboarding/Webhook.php b/src/Core/Onboarding/Webhook.php index b12e7f6e..b56cee55 100644 --- a/src/Core/Onboarding/Webhook.php +++ b/src/Core/Onboarding/Webhook.php @@ -71,7 +71,7 @@ public function registerWebhooks(): string /** @var GenericService $notificationService */ $webhookService = Registry::get(ServiceFactory::class)->getWebhookService(); - $webHookResponse = $webhookService->request('post', $paypload); + $webHookResponse = $webhookService->request('POST', $paypload); $webhookId = $webHookResponse['id'] ?? ''; } catch (Exception $exception) { @@ -119,7 +119,7 @@ public function getAllRegisteredWebhooks(): array { /** @var GenericService $notificationService */ $webhookService = Registry::get(ServiceFactory::class)->getWebhookService(); - $result = $webhookService->request('get'); + $result = $webhookService->request('GET'); return $result['webhooks'] ?? []; } diff --git a/src/Core/Tracker/Tracker.php b/src/Core/Tracker/Tracker.php index c90a2850..8954852a 100644 --- a/src/Core/Tracker/Tracker.php +++ b/src/Core/Tracker/Tracker.php @@ -49,7 +49,7 @@ public function sendtracking( /** @var GenericService $notificationService */ $trackerService = Registry::get(ServiceFactory::class)->getTrackerService(); - $trackerResponse = $trackerService->request('post', $paypload); + $trackerResponse = $trackerService->request('POST', $paypload); $result = $trackerResponse['tracker_identifiers'][0]['tracking_number'] === $trackingNumber; } catch (\Exception $exception) { diff --git a/src/Core/Webhook/EventVerifier.php b/src/Core/Webhook/EventVerifier.php index 48ff51b3..ed3e4224 100644 --- a/src/Core/Webhook/EventVerifier.php +++ b/src/Core/Webhook/EventVerifier.php @@ -62,7 +62,7 @@ public function verify(array $headers, string $body): bool /** @var GenericService $notificationService */ $notificationService = Registry::get(ServiceFactory::class)->getNotificationService(); - $response = $notificationService->request('post', $payload); + $response = $notificationService->request('POST', $payload); if ( !$response['verification_status'] || ( From 344cd9ce42c29308c705203554f617a0aa76d633 Mon Sep 17 00:00:00 2001 From: Mario Lorenz Date: Tue, 16 Jul 2024 16:44:08 +0200 Subject: [PATCH 07/13] CHANGELOG --- CHANGELOG.md | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ca13177..0718ab90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,34 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -## [3.3.5] - 2024-??-?? +## [2.5.0] - 2024-??-?? + +### FIX + +- Fix admin block parent call, thanks to Alpha-Sys +- Fix Errorlog-Message "Duplicate entry ..." + fix Update send PUI-Bankdata via Webhook +- Fix PayPalExpress Reauth is necessary if the cart amount (total is greater than before) has changed during the checkout process +- Fix, don't show vaulting-Boxes if it is deactivated in Backend +- [0007656](https://bugs.oxid-esales.com/view.php?id=7656): Fix incompatibility with Klarna-Module +- better Vaulting-Check in PaymentController +- disable Vaulting-Setting if Vaulting not possible +- [0007666](https://bugs.oxid-esales.com/view.php?id=7666): Fix: Price surcharges on the detail page for selection lists are not taken into account +- disable Vaulting-Option of Creditcard if Creditcard are not eligible +- Automatically save Apple Pay certificates during the Apple Pay eligibility check +- [0007681](https://bugs.oxid-esales.com/view.php?id=7681): fix OXID Logger.ERROR: Call to a member function getFieldData() on bool +- [0007675](https://bugs.oxid-esales.com/view.php?id=7675): fix the possibility to finish order without redirect and login to Paypal +- [0007676](https://bugs.oxid-esales.com/view.php?id=7676): If we have a corrupted generated_services.yaml and try to deactivate the module via the admin, we will display a more understandable error message about what happened. +- introduce ActionHash to make the PayPal-Request-ID more unique +- use PayPal-Client v2.0.15 + +### NEW +- PayPal-Request-Id based on serialized body, no extra PayPal-Request-Id necessary anymore +- Introduce GooglePay-Payment +- Introduce ApplePay-Payment +- use PayPal-Client v2.0.14 +- add Default-Shippingcosts for PP-Express to prevent overcharge. + +## [2.4.0] - 2024-04-04 ### FIX @@ -18,7 +45,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - [0007666](https://bugs.oxid-esales.com/view.php?id=7666): Fix: Price surcharges on the detail page for selection lists are not taken into account - [0007695](https://bugs.oxid-esales.com/view.php?id=7695): Fix: if DeliverySet is set in Frontend, then do not add any PseudoDeliveryCosts for PPExpress -### NEW +### NEW - provide Smarty-Templates again for OXID7.0 - thank you to D3-Team - use PayPal-Request-ID in any API-Call (via Client, v3.0.10) From 4afaacca5bfba901a447395ff75ec4ee23ca2431 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Gust?= Date: Tue, 8 Oct 2024 18:08:19 +0200 Subject: [PATCH 08/13] PSPAYPAL-812 merge CHANGELOG.md --- CHANGELOG.md | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0718ab90..0cab4e7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -## [2.5.0] - 2024-??-?? +## [3.3.5] - 2024-??-?? ### FIX @@ -23,18 +23,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - [0007676](https://bugs.oxid-esales.com/view.php?id=7676): If we have a corrupted generated_services.yaml and try to deactivate the module via the admin, we will display a more understandable error message about what happened. - introduce ActionHash to make the PayPal-Request-ID more unique - use PayPal-Client v2.0.15 - -### NEW -- PayPal-Request-Id based on serialized body, no extra PayPal-Request-Id necessary anymore -- Introduce GooglePay-Payment -- Introduce ApplePay-Payment -- use PayPal-Client v2.0.14 -- add Default-Shippingcosts for PP-Express to prevent overcharge. - -## [2.4.0] - 2024-04-04 - -### FIX - - [0007588](https://bugs.oxid-esales.com/view.php?id=7588): Improve Error handling for Capture Order Requests (thanks to mount7) - remove Sofort and MyBank, Paymentmethods will soon no longer be accepted via PayPal - fix: Refund only with note to Buyer (required) @@ -46,7 +34,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - [0007695](https://bugs.oxid-esales.com/view.php?id=7695): Fix: if DeliverySet is set in Frontend, then do not add any PseudoDeliveryCosts for PPExpress ### NEW - +- PayPal-Request-Id based on serialized body, no extra PayPal-Request-Id necessary anymore +- Introduce GooglePay-Payment +- Introduce ApplePay-Payment +- use PayPal-Client v2.0.14 +- add Default-Shippingcosts for PP-Express to prevent overcharge. - provide Smarty-Templates again for OXID7.0 - thank you to D3-Team - use PayPal-Request-ID in any API-Call (via Client, v3.0.10) - add Default-Shippingcosts for PP-Express to prevent overcharge. From 322d851aafab8109eb80f24aa63f674ea679e282 Mon Sep 17 00:00:00 2001 From: Mario Lorenz Date: Tue, 16 Jul 2024 16:32:08 +0200 Subject: [PATCH 09/13] Fetch more Informations for Order. If Order is completed no capture are necessary. In this case return the fetched Order. --- src/Service/Payment.php | 64 ++++++++++++++++++++++++++++++++++------- 1 file changed, 53 insertions(+), 11 deletions(-) diff --git a/src/Service/Payment.php b/src/Service/Payment.php index b88eb239..c28eb5d2 100644 --- a/src/Service/Payment.php +++ b/src/Service/Payment.php @@ -30,6 +30,7 @@ use OxidSolutionCatalysts\PayPalApi\Model\Orders\Order; use OxidSolutionCatalysts\PayPalApi\Model\Orders\Order as ApiModelOrder; use OxidSolutionCatalysts\PayPalApi\Model\Orders\Order as ApiOrderModel; +use OxidSolutionCatalysts\PayPalApi\Model\Orders\Order as OrderResponse; use OxidSolutionCatalysts\PayPalApi\Model\Orders\OrderAuthorizeRequest; use OxidSolutionCatalysts\PayPalApi\Model\Orders\OrderCaptureRequest; use OxidSolutionCatalysts\PayPalApi\Model\Payments\CaptureRequest; @@ -227,9 +228,9 @@ public function doCapturePayPalOrder( ): ApiOrderModel { /** @var ApiOrderModel $payPalOrder */ - $payPalOrder = is_null($payPalOrder) || !isset($payPalOrder->payment_source) ? - $this->fetchOrderFields($checkoutOrderId, 'payment_source') : - $payPalOrder; + if (is_null($payPalOrder) || !isset($payPalOrder->payment_source)) { + $payPalOrder = $this->fetchOrderFields($checkoutOrderId); + } //Verify 3D result if acdc payment if (!$this->verify3D($paymentId, $payPalOrder)) { @@ -302,17 +303,23 @@ public function doCapturePayPalOrder( } $result = $this->fetchOrderFields($checkoutOrderId); - } else { + } elseif ($payPalOrder->status !== Constants::PAYPAL_STATUS_COMPLETED) { $request = new OrderCaptureRequest(); //order number must be resolved before order patching $shopOrderId = $order->getFieldData('oxordernr'); if(!$shopOrderId){ $order->setOrderNumber(); - $shopOrderId = $order->getFieldData('oxordernr'); } try { - /** @var ApiOrderModel */ + //Patching the order with OXID order number as custom value + $this->doPatchPayPalOrder( + Registry::getSession()->getBasket(), + $checkoutOrderId, + $order->getFieldData('oxordernr'), + $shopOrderId + ); + /** @var $result ApiOrderModel */ $result = $orderService->capturePaymentForOrder( '', $checkoutOrderId, @@ -328,8 +335,12 @@ public function doCapturePayPalOrder( $this->logger->log('debug', $exception->getMessage(), [$exception]); throw oxNew(StandardException::class, 'OSC_PAYPAL_ORDEREXECUTION_ERROR'); } + } else { + // Order is captured, so we set the provided payPalOrder as result + $result = $payPalOrder; } + $payPalTransactionId = $result && isset($result->purchase_units[0]->payments->captures[0]->id) ? $result->purchase_units[0]->payments->captures[0]->id : ''; @@ -346,7 +357,38 @@ public function doCapturePayPalOrder( ); if ($result instanceof Order && $order->isPayPalOrderCompleted($result)) { - $order->setOrderNumber(); + //save vault to user and set success message + $session = Registry::getSession(); + $vault = null; + + if ($paypal = $result->payment_source->paypal) { + $vault = $paypal->attributes->vault; + } elseif ($card = $result->payment_source->card) { + $vault = $card->attributes->vault; + } + + if ($session->getVariable("vaultSuccess") && $vault->status === "VAULTED") { + $vaultSuccess = false; + + if ($id = $vault->customer["id"]) { + $user = Registry::getConfig()->getUser(); + + $user->oxuser__oscpaypalcustomerid = new Field($id); + + if ($user->save()) { + $vaultSuccess = true; + } + } + + if (!$vaultSuccess) { + $this->logger->log('debug', "Vaulting was attempted but didn't succeed."); + } + + $session->setVariable("vaultSuccess", $vaultSuccess); + } else { + $session->deleteVariable("vaultSuccess"); + } + $order->markOrderPaid(); $order->setTransId((string)$payPalTransactionId); } @@ -728,13 +770,13 @@ public function verify3D(string $paymentId, ApiOrderModel $payPalOrder): bool private function handlePayPalApiError(ApiException $exception): void { $issue = $exception->getErrorIssue(); - if (self::PAYMENT_SOURCE_INFO_CANNOT_BE_VERIFIED == 'PUI_' . $issue) { + if (self::PAYMENT_SOURCE_INFO_CANNOT_BE_VERIFIED === 'PUI_' . $issue) { $this->setPaymentExecutionError(self::PAYMENT_SOURCE_INFO_CANNOT_BE_VERIFIED); - } elseif (self::PAYMENT_SOURCE_DECLINED_BY_PROCESSOR == 'PUI_' . $issue) { + } elseif (self::PAYMENT_SOURCE_DECLINED_BY_PROCESSOR === 'PUI_' . $issue) { $this->setPaymentExecutionError(self::PAYMENT_SOURCE_DECLINED_BY_PROCESSOR); - } elseif (PayPalDefinitions::PUI_PAYPAL_PAYMENT_ID == $this->getSessionPaymentId()) { + } elseif (PayPalDefinitions::PUI_PAYPAL_PAYMENT_ID === $this->getSessionPaymentId()) { $this->setPaymentExecutionError(self::PAYMENT_ERROR_PUI_GENERIC); - } elseif (self::PAYMENT_ERROR_INSTRUMENT_DECLINED == 'PAYPAL_ERROR_' . $issue) { + } elseif (self::PAYMENT_ERROR_INSTRUMENT_DECLINED === 'PAYPAL_ERROR_' . $issue) { $this->setPaymentExecutionError(self::PAYMENT_ERROR_INSTRUMENT_DECLINED); } else { $this->setPaymentExecutionError(self::PAYMENT_ERROR_GENERIC); From 540856ac71664cabc42dff8b7c7db17a11f46f9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Gust?= Date: Tue, 8 Oct 2024 18:28:04 +0200 Subject: [PATCH 10/13] PSPAYPAL-812 merge Payment.php --- src/Service/Payment.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Service/Payment.php b/src/Service/Payment.php index c28eb5d2..985a7452 100644 --- a/src/Service/Payment.php +++ b/src/Service/Payment.php @@ -316,8 +316,7 @@ public function doCapturePayPalOrder( $this->doPatchPayPalOrder( Registry::getSession()->getBasket(), $checkoutOrderId, - $order->getFieldData('oxordernr'), - $shopOrderId + $order->getFieldData('oxordernr') ); /** @var $result ApiOrderModel */ $result = $orderService->capturePaymentForOrder( From b690098414044f242c08764d018c57231b288693 Mon Sep 17 00:00:00 2001 From: Mario Lorenz Date: Tue, 16 Jul 2024 16:30:28 +0200 Subject: [PATCH 11/13] refactor code for better debugging --- .../Handler/CheckoutOrderApprovedHandler.php | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/Core/Webhook/Handler/CheckoutOrderApprovedHandler.php b/src/Core/Webhook/Handler/CheckoutOrderApprovedHandler.php index 04909405..a8d3d0db 100644 --- a/src/Core/Webhook/Handler/CheckoutOrderApprovedHandler.php +++ b/src/Core/Webhook/Handler/CheckoutOrderApprovedHandler.php @@ -62,15 +62,13 @@ protected function getPayPalOrderIdFromResource(array $eventPayload): string protected function getPayPalTransactionIdFromResource(array $eventPayload): string { - $transactionId = isset($eventPayload['payments']['captures'][0]) ? + return isset($eventPayload['payments']['captures'][0]) ? $eventPayload['payments']['captures'][0]['id'] : ''; - - return $transactionId; } protected function getStatusFromResource(array $eventPayload): string { - return isset($eventPayload['status']) ? $eventPayload['status'] : ''; + return $eventPayload['status'] ?? ''; } /** @@ -106,11 +104,9 @@ private function needsCapture(array $eventPayload): bool private function isCompleted(array $eventPayload): bool { - return ( - isset($eventPayload['status']) && - isset($eventPayload['purchase_units'][0]['payments']['captures'][0]['status']) && - $this->getStatusFromResource($eventPayload) == OrderResponse::STATUS_COMPLETED && - $eventPayload['purchase_units'][0]['payments']['captures'][0]['status'] == Capture::STATUS_COMPLETED - ); + $condition1 = isset($eventPayload['status'], $eventPayload['purchase_units'][0]['payments']['captures'][0]['status']); + $condition2 = $this->getStatusFromResource($eventPayload) === OrderResponse::STATUS_COMPLETED; + $condition3 = $eventPayload['purchase_units'][0]['payments']['captures'][0]['status'] === Capture::STATUS_COMPLETED; + return ($condition1 && $condition2 && $condition3); } } From 7d64787bc69ae56332662e5d90250d47a8fa1a96 Mon Sep 17 00:00:00 2001 From: Mario Lorenz Date: Tue, 16 Jul 2024 16:20:44 +0200 Subject: [PATCH 12/13] fix: AmountPatch only if Amount != 0,0 --- src/Core/PatchRequestFactory.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Core/PatchRequestFactory.php b/src/Core/PatchRequestFactory.php index e0f99171..94ed8d10 100644 --- a/src/Core/PatchRequestFactory.php +++ b/src/Core/PatchRequestFactory.php @@ -15,7 +15,6 @@ use OxidEsales\Eshop\Application\Model\Country; use OxidEsales\Eshop\Application\Model\State; use OxidEsales\Eshop\Core\Registry; -use OxidSolutionCatalysts\PayPal\Core\PayPalRequestAmountFactory; use OxidSolutionCatalysts\PayPalApi\Model\Orders\AddressPortable; use OxidSolutionCatalysts\PayPalApi\Model\Orders\Item; use OxidSolutionCatalysts\PayPalApi\Model\Orders\Patch; @@ -127,12 +126,15 @@ protected function getShippingNamePatch(): void protected function getAmountPatch(): void { - $patch = new Patch(); - $patch->op = Patch::OP_REPLACE; - $patch->path = "/purchase_units/@reference_id=='" . Constants::PAYPAL_ORDER_REFERENCE_ID . "'/amount"; - $patch->value = (Registry::get(PayPalRequestAmountFactory::class))->getAmount($this->basket); + $value = (Registry::get(PayPalRequestAmountFactory::class))->getAmount($this->basket); + if ((float)$value->value !== 0.00) { + $patch = new Patch(); + $patch->op = Patch::OP_REPLACE; + $patch->path = "/purchase_units/@reference_id=='" . Constants::PAYPAL_ORDER_REFERENCE_ID . "'/amount"; + $patch->value = $value; - $this->request[] = $patch; + $this->request[] = $patch; + } } /** @@ -152,7 +154,7 @@ protected function getPurchaseUnitsPatch( $item = new Item(); $item->name = $basketItem->getTitle(); $itemUnitPrice = $basketItem->getUnitPrice(); - $item->unit_amount = PriceToMoney::convert((float)$itemUnitPrice->getBruttoPrice(), $currency); + $item->unit_amount = PriceToMoney::convert($itemUnitPrice->getBruttoPrice(), $currency); //Item tax sum - we use 0% and calculate with brutto to avoid rounding errors $item->tax = PriceToMoney::convert(0, $currency); From 66580362aff2db4ad65307a7966f8692d1e2ee6c Mon Sep 17 00:00:00 2001 From: Mario Lorenz Date: Tue, 16 Jul 2024 16:19:55 +0200 Subject: [PATCH 13/13] add actionHash to make Request Unique --- src/Core/ServiceFactory.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Core/ServiceFactory.php b/src/Core/ServiceFactory.php index 021da211..c604bbce 100644 --- a/src/Core/ServiceFactory.php +++ b/src/Core/ServiceFactory.php @@ -9,6 +9,7 @@ namespace OxidSolutionCatalysts\PayPal\Core; +use OxidEsales\Eshop\Core\Registry; use OxidSolutionCatalysts\PayPal\Traits\ServiceContainer; use OxidSolutionCatalysts\PayPalApi\Client; use OxidSolutionCatalysts\PayPalApi\Service\Partner; @@ -120,12 +121,21 @@ private function getClient(): Client /** @var LoggerInterface $logger */ $logger = $this->getServiceFromContainer('OxidSolutionCatalysts\PayPal\Logger'); + $debug = Registry::getConfig()->getConfigParam('sLogLevel') === 'debug'; + + // prepare a unique action hash + $session = Registry::getSession(); + $sessionId = $session->getId(); + $basketId = $session->getVariable('sess_challenge'); + $actionHash = md5($sessionId . $basketId); + $client = new Client( $logger, $config->isSandbox() ? Client::SANDBOX_URL : Client::PRODUCTION_URL, $config->getClientId(), $config->getClientSecret(), $config->getTokenCacheFileName(), + $actionHash, // must be empty. We do not have the merchant's payerid //and confirmed by paypal we should not use it for auth and //so not ask for it on the configuration page