From 05c83b1ac60b82112aab4cf85e1e6a377f5337ea Mon Sep 17 00:00:00 2001 From: andrewrowanwallee Date: Thu, 28 May 2026 17:34:20 +0200 Subject: [PATCH] Release 7.3.4 --- CHANGELOG.md | 8 ++ CHANGELOG_de-DE.md | 8 ++ README.md | 8 +- composer.json | 2 +- docs/de/documentation.html | 2 +- docs/en/documentation.html | 2 +- docs/fr/documentation.html | 2 +- docs/it/documentation.html | 2 +- .../Service/TransactionService.php | 129 +++++++++++++++++- .../WebHooks/Controller/WebHookController.php | 10 -- .../WebHooks/Strategy/WebHookStrategyBase.php | 10 -- .../VRPaymentPaymentHandler.php | 31 ++++- .../Service/TransactionManagementService.php | 5 +- .../StoreApi/Route/CartRecoveryRoute.php | 21 ++- .../Controller/CheckoutController.php | 7 + src/Core/Util/Analytics/Analytics.php | 2 +- .../config/services/core/store_api.xml | 1 + 17 files changed, 206 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce5b0a9..e787ec1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# 7.3.4 +- Shopware 6.7.10.0 compatible +- Fix for cart being lost when changing payment methods +- Fixed for incorrectly reference function (getState()) +- Fix cache not being cleaned after payment errors +- Fix missing shipping costs +- Minor fix for Customer Id null inconsistency + # 7.3.3 - Shopware 6.7.9.0 compatible - Fix for only showing 25 sales channels in selector diff --git a/CHANGELOG_de-DE.md b/CHANGELOG_de-DE.md index 47e3d4d..6fc9485 100644 --- a/CHANGELOG_de-DE.md +++ b/CHANGELOG_de-DE.md @@ -1,3 +1,11 @@ +# 7.3.4 +- Kompatibel mit Shopware 6.7.10.0 +- Problem behoben, bei dem der Warenkorb beim Ändern der Zahlungsmethode verloren ging +- Fehlerhafte Referenzierung der Funktion getState() behoben +- Problem behoben, bei dem der Cache nach Zahlungsfehlern nicht geleert wurde +- Fehlende Versandkosten behoben +- Kleinere Korrektur eines Fehlers mit der Kunden-ID (null) + # 7.3.3 - Kompatibel mit Shopware 6.7.9.0 - Problem behoben, dass im Selektor nur 25 Vertriebskanäle angezeigt wurden diff --git a/README.md b/README.md index cdc6a8c..cec96e4 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,10 @@ Please note that this plugin is for versions 6.5, 6.6 or 6.7. For the 6.4 plugin ## Documentation -- For English documentation click [here](https://docs.plugin-documentation.vr-payment.de/vr-payment/shopware-6/7.3.3/docs/en/documentation.html) -- Für die deutsche Dokumentation klicken Sie [hier](https://docs.plugin-documentation.vr-payment.de/vr-payment/shopware-6/7.3.3/docs/de/documentation.html) -- Pour la documentation Française, cliquez [ici](https://docs.plugin-documentation.vr-payment.de/vr-payment/shopware-6/7.3.3/docs/fr/documentation.html) -- Per la documentazione in tedesco, clicca [qui](https://docs.plugin-documentation.vr-payment.de/vr-payment/shopware-6/7.3.3/docs/it/documentation.html) +- For English documentation click [here](https://docs.plugin-documentation.vr-payment.de/vr-payment/shopware-6/7.3.4/docs/en/documentation.html) +- Für die deutsche Dokumentation klicken Sie [hier](https://docs.plugin-documentation.vr-payment.de/vr-payment/shopware-6/7.3.4/docs/de/documentation.html) +- Pour la documentation Française, cliquez [ici](https://docs.plugin-documentation.vr-payment.de/vr-payment/shopware-6/7.3.4/docs/fr/documentation.html) +- Per la documentazione in tedesco, clicca [qui](https://docs.plugin-documentation.vr-payment.de/vr-payment/shopware-6/7.3.4/docs/it/documentation.html) ## Installation diff --git a/composer.json b/composer.json index 1a0d5ab..25dfff9 100644 --- a/composer.json +++ b/composer.json @@ -59,5 +59,5 @@ "vrpayment/sdk": "^4.0.0" }, "type": "shopware-platform-plugin", - "version": "7.3.3" + "version": "7.3.4" } diff --git a/docs/de/documentation.html b/docs/de/documentation.html index 778f8fa..f97b6c1 100644 --- a/docs/de/documentation.html +++ b/docs/de/documentation.html @@ -23,7 +23,7 @@
  • - + Source
  • diff --git a/docs/en/documentation.html b/docs/en/documentation.html index 4d7b37d..a2b2c4f 100644 --- a/docs/en/documentation.html +++ b/docs/en/documentation.html @@ -23,7 +23,7 @@
  • - + Source
  • diff --git a/docs/fr/documentation.html b/docs/fr/documentation.html index 6f62905..a5a3bb2 100644 --- a/docs/fr/documentation.html +++ b/docs/fr/documentation.html @@ -23,7 +23,7 @@
  • - + Source
  • diff --git a/docs/it/documentation.html b/docs/it/documentation.html index 4d44c60..8a38301 100644 --- a/docs/it/documentation.html +++ b/docs/it/documentation.html @@ -23,7 +23,7 @@
  • - + Source
  • diff --git a/src/Core/Api/Transaction/Service/TransactionService.php b/src/Core/Api/Transaction/Service/TransactionService.php index 836f69a..6937ae7 100644 --- a/src/Core/Api/Transaction/Service/TransactionService.php +++ b/src/Core/Api/Transaction/Service/TransactionService.php @@ -32,6 +32,7 @@ use VRPayment\Sdk\Model\{ LineItemAttributeCreate, LineItemCreate, LineItemType, + TaxCreate, TokenizationMode, Transaction, TransactionCreate, @@ -606,9 +607,17 @@ class TransactionService if ($customer === null) { throw new \Exception('Customer is required to create a transaction'); } - $lineItems = $this->extractLineItems($event); + $lineItems = $this->extractLineItems( + $event, + $salesChannelContext, + ); - $customerId = ""; + /* + * For guest checkouts, the customer ID is set to null rather than an empty string. + * This ensures consistency with TransactionPayload which also uses null for guests, + * preventing the Portal from treating the difference as an update/change in customer details. + */ + $customerId = null; if ($customer->getGuest() === false) { $customerId = $customer->getCustomerNumber(); } @@ -690,13 +699,16 @@ class TransactionService } /** - * Extracts line items from the given source (Event or Cart). + * Extracts line items from the given source (Event or Cart) and appends shipping costs. * * @param mixed $source + * @param SalesChannelContext|null $salesChannelContext * @return array */ - public function extractLineItems($source): array - { + public function extractLineItems( + $source, + ?SalesChannelContext $salesChannelContext = null, + ): array { $lineItems = []; if ($source) { if ($source instanceof CheckoutConfirmPageLoadedEvent) { @@ -721,6 +733,38 @@ class TransactionService $lineItems[] = $this->createTempLineItem($cartLineItem); } } + + // Extract and append shipping costs as a line item if applicable. + $shippingCosts = null; + $taxStatus = 'gross'; + + if ($source instanceof CheckoutConfirmPageLoadedEvent) { + $cart = $source->getPage()->getCart(); + $shippingCosts = $cart->getDeliveries()->getShippingCosts(); + if ($salesChannelContext !== null) { + $taxStatus = $salesChannelContext->getTaxState(); + } + } elseif ($source instanceof AccountEditOrderPageLoadedEvent) { + $order = $source->getPage()->getOrder(); + $shippingCosts = $order->getShippingCosts(); + $taxStatus = $order->getTaxStatus(); + } elseif ($source instanceof \Shopware\Core\Checkout\Cart\Cart) { + $shippingCosts = $source->getDeliveries()->getShippingCosts(); + if ($salesChannelContext !== null) { + $taxStatus = $salesChannelContext->getTaxState(); + } + } + + if ($shippingCosts !== null) { + $shippingLineItem = $this->extractShippingLineItem( + $shippingCosts, + $taxStatus, + $salesChannelContext, + ); + if ($shippingLineItem !== null) { + $lineItems[] = $shippingLineItem; + } + } } return $lineItems; } @@ -862,6 +906,7 @@ class TransactionService $address->setOrganizationName($addressEntity->getCompany()); $address->setPhoneNumber($addressEntity->getPhoneNumber()); $address->setCountry($addressEntity->getCountry()->getIso()); + $address->setCity($addressEntity->getCity() ?: ''); $postalState = $addressEntity?->getCountryState()?->getName() ?: $addressEntity?->getCountryState()?->getShortCode() @@ -967,7 +1012,7 @@ class TransactionService * * @param SalesChannelContext $salesChannelContext */ - private function clearTransactionIdFromContext(SalesChannelContext $salesChannelContext): void + public function clearTransactionIdFromContext(SalesChannelContext $salesChannelContext): void { // Clear from cache key. $cacheKey = $this->getPendingTransactionCacheKey($salesChannelContext); @@ -1028,4 +1073,76 @@ class TransactionService return $lineItem; } + + /** + * Extracts shipping line item from cart/order shipping costs. + * + * @param mixed $shippingCosts + * @param string $taxStatus + * @param SalesChannelContext|null $salesChannelContext + * @return LineItemCreate|null + */ + private function extractShippingLineItem( + $shippingCosts, + string $taxStatus, + ?SalesChannelContext $salesChannelContext = null, + ): ?LineItemCreate { + // When shipping costs are extracted from a Cart, they are returned as a PriceCollection + // containing multiple CalculatedPrice items. We must sum them to get a single aggregated price. + if ($shippingCosts instanceof \Shopware\Core\Checkout\Cart\Price\Struct\PriceCollection) { + $shippingCosts = $shippingCosts->sum(); + } + + if (!$shippingCosts instanceof \Shopware\Core\Checkout\Cart\Price\Struct\CalculatedPrice) { + return null; + } + + if ($shippingCosts->getTotalPrice() <= 0) { + return null; + } + + $amount = $shippingCosts->getTotalPrice(); + if ($taxStatus === 'net') { + $amount += $shippingCosts->getCalculatedTaxes()->getAmount(); + } + $roundedAmount = $this->round($amount); + + $shippingMethodName = $salesChannelContext?->getShippingMethod()?->getName(); + $translator = $this->container->has('translator') ? $this->container->get('translator') : null; + $fallbackName = $translator ? $translator->trans('vrpayment.payload.shipping.name') : 'Shipping'; + $shippingName = $shippingMethodName ?? $fallbackName; + + $shippingLineItem = new LineItemCreate(); + $shippingLineItem->setAmountIncludingTax($roundedAmount) + ->setName($this->fixLength($shippingName . ' ' . ($translator ? $translator->trans('vrpayment.payload.shipping.lineItem') : 'Shipping'), 150)) + ->setQuantity($shippingCosts->getQuantity() ?? 1) + ->setSku($this->fixLength($shippingName . '-Shipping', 200)) + ->setType(LineItemType::SHIPPING) + ->setUniqueId($this->fixLength($shippingName . '-Shipping', 200)); + + if ($taxStatus !== 'tax-free') { + $taxes = []; + foreach ($shippingCosts->getCalculatedTaxes() as $calculatedTax) { + $tax = (new TaxCreate()) + ->setRate($calculatedTax->getTaxRate()) + ->setTitle($this->fixLength($shippingName . ' : ' . $calculatedTax->getTaxRate(), 40)); + $taxes[] = $tax; + } + $shippingLineItem->setTaxes($taxes); + } + + return $shippingLineItem; + } + + /** + * Fix string length to specific length. + * + * @param string $string + * @param int $maxLength + * @return string + */ + private function fixLength(string $string, int $maxLength): string + { + return \mb_substr($string, 0, $maxLength, 'UTF-8'); + } } diff --git a/src/Core/Api/WebHooks/Controller/WebHookController.php b/src/Core/Api/WebHooks/Controller/WebHookController.php index 2f2636b..118f4bd 100644 --- a/src/Core/Api/WebHooks/Controller/WebHookController.php +++ b/src/Core/Api/WebHooks/Controller/WebHookController.php @@ -700,16 +700,6 @@ class WebHookController extends AbstractController { private function unholdAndCancelDelivery(string $orderId, Context $context): void { $order = $this->getOrderEntity($orderId, $context); - try { - $this->orderService->orderStateTransition( - $order->getId(), - StateMachineTransitionActions::ACTION_CANCEL, - new ParameterBag(), - $context - ); - } catch (\Exception $exception) { - $this->logger->info($exception->getMessage(), $exception->getTrace()); - } try { /** diff --git a/src/Core/Api/WebHooks/Strategy/WebHookStrategyBase.php b/src/Core/Api/WebHooks/Strategy/WebHookStrategyBase.php index e77e898..3c29cd2 100644 --- a/src/Core/Api/WebHooks/Strategy/WebHookStrategyBase.php +++ b/src/Core/Api/WebHooks/Strategy/WebHookStrategyBase.php @@ -378,16 +378,6 @@ abstract class WebHookStrategyBase implements WebHookStrategyInterface { protected function unholdAndCancelDelivery(string $orderId, Context $context): void { $order = $this->getOrderEntity($orderId, $context); - try { - $this->orderService->orderStateTransition( - $order->getId(), - StateMachineTransitionActions::ACTION_CANCEL, - new ParameterBag(), - $context - ); - } catch (\Exception $exception) { - $this->logger->info($exception->getMessage(), $exception->getTrace()); - } try { diff --git a/src/Core/Checkout/PaymentHandler/VRPaymentPaymentHandler.php b/src/Core/Checkout/PaymentHandler/VRPaymentPaymentHandler.php index 4057afa..953c406 100644 --- a/src/Core/Checkout/PaymentHandler/VRPaymentPaymentHandler.php +++ b/src/Core/Checkout/PaymentHandler/VRPaymentPaymentHandler.php @@ -151,10 +151,18 @@ class VRPaymentPaymentHandler extends AbstractPaymentHandler } return new RedirectResponse($redirectUrl); } catch (\Throwable $e) { - $request->getSession()->remove('transactionId'); + // Clear the transaction ID from the cache or session context depending on whether the SalesChannelContext was initialized + // to prevent subsequent checkout attempts from reusing a failed/invalid transaction ID. + if (isset($salesChannelContext)) { + $this->pluginTransactionService->clearTransactionIdFromContext($salesChannelContext); + } else { + $request->getSession()->remove('transactionId'); + } $errorMessage = 'An error occurred during the communication with external payment gateway : ' . $e->getMessage(); $this->logger->critical($errorMessage); - if ($orderTransaction->getState()?->getTechnicalName() === OrderTransactionStates::STATE_CANCELLED) { + // If the transaction has already been marked as cancelled (e.g. via webhook or concurrent request), + // throw an interrupted exception to signal that payment processing cannot be finalized normally. + if ($orderTransaction->getStateMachineState()?->getTechnicalName() === OrderTransactionStates::STATE_CANCELLED) { throw PaymentException::asyncFinalizeInterrupted($orderTransaction->getOrder()->getId(), $errorMessage); } throw PaymentException::customerCanceled($transaction->getOrderTransactionId(), $errorMessage); @@ -196,12 +204,27 @@ class VRPaymentPaymentHandler extends AbstractPaymentHandler $transactionEntity->getSalesChannelId() ); - if (in_array($vRPaymentTransaction->getState(), [TransactionState::FAILED])) { + if (in_array($vRPaymentTransaction->getState(), [TransactionState::FAILED, TransactionState::DECLINE, TransactionState::VOIDED,])) { $errorMessage = strtr('Customer canceled payment for :orderId on SalesChannel :salesChannelName', [ ':orderId' => $orderTransaction->getOrder()->getId(), ':salesChannelName' => $transactionEntity->getSalesChannelId(), ]); - $request->getSession()->remove('transactionId'); + + // Retrieve the sales channel context parameters to clear the transaction ID from cache or session context, + // ensuring that a failed transaction is not reused when the customer retries payment. + $token = $this->getContextToken($request); + if ($token) { + $parameters = new SalesChannelContextServiceParameters( + $transactionEntity->getSalesChannelId(), + $token, + originalContext: $context, + ); + $salesChannelContext = $this->salesChannelContextService->get($parameters); + $this->pluginTransactionService->clearTransactionIdFromContext($salesChannelContext); + } else { + $request->getSession()->remove('transactionId'); + } + $this->logger->info($errorMessage); throw PaymentException::customerCanceled($orderTransactionId, $errorMessage); } diff --git a/src/Core/Checkout/Service/TransactionManagementService.php b/src/Core/Checkout/Service/TransactionManagementService.php index a2bad81..95dc9ea 100644 --- a/src/Core/Checkout/Service/TransactionManagementService.php +++ b/src/Core/Checkout/Service/TransactionManagementService.php @@ -112,7 +112,10 @@ class TransactionManagementService $addressHash = $customer ? md5(json_encode((array) $customer)) : null; $currency = (string)$salesChannelContext->getCurrency()->getIsoCode(); - $lineItems = $this->transactionService->extractLineItems($event); + $lineItems = $this->transactionService->extractLineItems( + $event, + $salesChannelContext, + ); $lineItemHash = !empty($lineItems) ? md5(json_encode($lineItems)) : $oldLineItemHash; $needsUpdate = ($oldAddressHash !== $addressHash) diff --git a/src/Core/Checkout/StoreApi/Route/CartRecoveryRoute.php b/src/Core/Checkout/StoreApi/Route/CartRecoveryRoute.php index 98f3039..7831b93 100644 --- a/src/Core/Checkout/StoreApi/Route/CartRecoveryRoute.php +++ b/src/Core/Checkout/StoreApi/Route/CartRecoveryRoute.php @@ -9,6 +9,7 @@ use Shopware\Core\System\SalesChannel\SalesChannelContext; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Attribute\Route; +use VRPaymentPayment\Core\Api\Transaction\Service\TransactionService; use VRPaymentPayment\Core\Checkout\Service\CartRecoveryService; #[Package('checkout')] @@ -26,11 +27,21 @@ class CartRecoveryRoute private CartRecoveryService $cartRecoveryService; /** - * @param CartRecoveryService $cartRecoveryService + * @var TransactionService + * Service to clear and manage transactions. */ - public function __construct(CartRecoveryService $cartRecoveryService) - { + private TransactionService $transactionService; + + /** + * @param CartRecoveryService $cartRecoveryService + * @param TransactionService $transactionService + */ + public function __construct( + CartRecoveryService $cartRecoveryService, + TransactionService $transactionService, + ) { $this->cartRecoveryService = $cartRecoveryService; + $this->transactionService = $transactionService; } /** @@ -57,6 +68,10 @@ class CartRecoveryRoute return new JsonResponse(['error' => 'Sales channel mismatch'], 403); } + // Clear the transaction ID from cache and session to prevent the subsequent + // checkout attempt from reusing a stale/failed transaction. + $this->transactionService->clearTransactionIdFromContext($context); + // Perform the cart reconstruction. $cart = $this->cartRecoveryService->recreateCartFromOrder($order, $context); diff --git a/src/Core/Storefront/Checkout/Controller/CheckoutController.php b/src/Core/Storefront/Checkout/Controller/CheckoutController.php index 366e0da..a084627 100644 --- a/src/Core/Storefront/Checkout/Controller/CheckoutController.php +++ b/src/Core/Storefront/Checkout/Controller/CheckoutController.php @@ -167,6 +167,9 @@ class CheckoutController extends StorefrontController if ($this->logger) { $this->logger->error($e->getMessage()); } + // Clear the transaction ID from the cache/session context on error to ensure + // subsequent payment attempts will create/retrieve a clean transaction. + $this->transactionService->clearTransactionIdFromContext($salesChannelContext); $this->addFlash('danger', $this->trans('vrpayment.paymentMethod.notAvailable')); return $this->redirectToRoute('frontend.home.page'); } @@ -219,6 +222,10 @@ class CheckoutController extends StorefrontController $this->addFlash('danger', $transaction->getUserFailureMessage()); } } + // Clear the transaction ID from cache and session to prevent the subsequent + // checkout attempt from reusing a stale/failed transaction. + $this->transactionService->clearTransactionIdFromContext($salesChannelContext); + $this->cartRecoveryService->recreateCartFromOrder($order, $salesChannelContext); } catch (\Exception $exception) { $this->addFlash('danger', $this->trans('error.addToCartError')); diff --git a/src/Core/Util/Analytics/Analytics.php b/src/Core/Util/Analytics/Analytics.php index 42ab496..b85978b 100644 --- a/src/Core/Util/Analytics/Analytics.php +++ b/src/Core/Util/Analytics/Analytics.php @@ -26,7 +26,7 @@ class Analytics { self::SHOP_SYSTEM => 'shopware', self::SHOP_SYSTEM_VERSION => '6', self::SHOP_SYSTEM_AND_VERSION => 'shopware-6', - self::PLUGIN_SYSTEM_VERSION => '7.3.3', + self::PLUGIN_SYSTEM_VERSION => '7.3.4', ]; } diff --git a/src/Resources/config/services/core/store_api.xml b/src/Resources/config/services/core/store_api.xml index f804de0..4edd3c1 100644 --- a/src/Resources/config/services/core/store_api.xml +++ b/src/Resources/config/services/core/store_api.xml @@ -18,6 +18,7 @@ +