Compare commits

...

2 Commits

Author SHA1 Message Date
andrewrowanwallee 05c83b1ac6 Release 7.3.4 2026-05-28 17:34:20 +02:00
andrewrowanwallee 93f24a2cac Release 7.3.3 2026-04-29 11:43:34 +02:00
30 changed files with 359 additions and 122 deletions
+13
View File
@@ -1,3 +1,16 @@
# 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
- Fixed bug with discount occasionally blocking payment methods
# 7.3.2
- Fix for partial refunds using standard refund option
- Fixed issue with undefined array key
+13
View File
@@ -1,3 +1,16 @@
# 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
- Fehler behoben, der gelegentlich dazu führte, dass Rabatte Zahlungsmethoden blockierten
# 7.3.2
- Problem mit Teilrückerstattungen über die Standardrückerstattungsoption behoben
- Problem mit undefiniertem Array-Schlüssel behoben
+4 -4
View File
@@ -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.2/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.2/docs/de/documentation.html)
- Pour la documentation Française, cliquez [ici](https://docs.plugin-documentation.vr-payment.de/vr-payment/shopware-6/7.3.2/docs/fr/documentation.html)
- Per la documentazione in tedesco, clicca [qui](https://docs.plugin-documentation.vr-payment.de/vr-payment/shopware-6/7.3.2/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
+1 -1
View File
@@ -59,5 +59,5 @@
"vrpayment/sdk": "^4.0.0"
},
"type": "shopware-platform-plugin",
"version": "7.3.2"
"version": "7.3.4"
}
+1 -1
View File
@@ -23,7 +23,7 @@
</a>
</li>
<li>
<a href="https://github.com/vr-payment/shopware-6/releases/tag/7.3.2/">
<a href="https://github.com/vr-payment/shopware-6/releases/tag/7.3.4/">
Source
</a>
</li>
+1 -1
View File
@@ -23,7 +23,7 @@
</a>
</li>
<li>
<a href="https://github.com/vr-payment/shopware-6/releases/tag/7.3.2/">
<a href="https://github.com/vr-payment/shopware-6/releases/tag/7.3.4/">
Source
</a>
</li>
+1 -1
View File
@@ -23,7 +23,7 @@
</a>
</li>
<li>
<a href="https://github.com/vr-payment/shopware-6/releases/tag/7.3.2/">
<a href="https://github.com/vr-payment/shopware-6/releases/tag/7.3.4/">
Source
</a>
</li>
+1 -1
View File
@@ -23,7 +23,7 @@
</a>
</li>
<li>
<a href="https://github.com/vr-payment/shopware-6/releases/tag/7.3.2/">
<a href="https://github.com/vr-payment/shopware-6/releases/tag/7.3.4/">
Source
</a>
</li>
@@ -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();
}
@@ -624,11 +633,14 @@ class TransactionService
throw new \Exception('Space settings not configured');
}
$language = $this->localeCodeProvider->getLocaleCodeFromContext($salesChannelContext->getContext());
$transactionPayload = (new TransactionCreate())
->setBillingAddress($billingAddress)
->setShippingAddress($shippingAddress)
->setLineItems($lineItems)
->setCurrency($currency)
->setLanguage($language)
->setSpaceViewId($settings->getSpaceViewId())
->setAutoConfirmationEnabled(false)
->setChargeRetryEnabled(false)
@@ -668,7 +680,10 @@ class TransactionService
$currency = $salesChannelContext->getCurrency()->getIsoCode();
$language = $this->localeCodeProvider->getLocaleCodeFromContext($salesChannelContext->getContext());
$pendingTransaction->setCurrency($currency);
$pendingTransaction->setLanguage($language);
$billingAddress = $this->buildAddress($salesChannelContext, $salesChannelContext->getCustomer()->getActiveBillingAddress());
$shippingAddress = $this->buildAddress($salesChannelContext, $salesChannelContext->getCustomer()->getActiveShippingAddress());
@@ -684,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) {
@@ -715,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;
}
@@ -807,20 +857,26 @@ class TransactionService
{
$lineItem = new LineItemCreate();
$roundedPrice = $this->round($productData->getPrice()->getUnitPrice());
$price = $productData->getPrice();
$unit = $price->getUnitPrice();
// Expects discounts as separate items, avoid negative prices
if ($unit < 0) {
return $this->mapDiscountLineItem($productData);
}
if ($productData instanceof LineItem) {
$lineItem->setName($productData->getLabel());
$lineItem->setUniqueId($productData->getId());
$lineItem->setSku($productData->getReferencedId() ?? $productData->getId());
$lineItem->setQuantity($productData->getQuantity());
$lineItem->setAmountIncludingTax($roundedPrice);
$lineItem->setAmountIncludingTax($this->round($unit));
} elseif ($productData instanceof OrderLineItemEntity) {
$lineItem->setName($productData->getLabel());
$lineItem->setUniqueId($productData->getId());
$lineItem->setSku($productData->getProductId() ?? $productData->getIdentifier() ?? $productData->getId());
$lineItem->setQuantity($productData->getQuantity());
$lineItem->setAmountIncludingTax($roundedPrice);
$lineItem->setAmountIncludingTax($this->round($unit));
} else {
throw new \InvalidArgumentException('Unsupported line item type: ' . get_class($productData));
}
@@ -850,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()
@@ -955,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);
@@ -991,4 +1048,101 @@ class TransactionService
// Store in session for Storefront.
$_SESSION['transactionId'] = $transactionId;
}
/**
* Creates a discount line item for negative-priced cart entries.
*
* @param $productData
* @return LineItemCreate
*
*/
private function mapDiscountLineItem($productData): LineItemCreate
{
$price = $productData->getPrice();
$lineItem = new LineItemCreate();
$amount = abs($price->getTotalPrice());
$lineItem->setName($productData->getLabel() ?: 'Discount');
$lineItem->setUniqueId('discount-' . $productData->getId());
$lineItem->setSku('discount');
$lineItem->setQuantity(1);
$lineItem->setAmountIncludingTax($this->round($amount));
$lineItem->setType(LineItemType::DISCOUNT);
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');
}
}
@@ -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 {
/**
@@ -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 {
@@ -137,7 +137,7 @@ class VRPaymentPaymentHandler extends AbstractPaymentHandler
}
$contextToken = $this->getContextToken($request);
$parameters = new SalesChannelContextServiceParameters($salesChannelContextId, $contextToken, originalContext: $context);
$parameters = new SalesChannelContextServiceParameters($salesChannelContextId, $contextToken, languageId: $context->getLanguageId(), originalContext: $context);
$salesChannelContext = $this->salesChannelContextService->get($parameters);
$redirectUrl = $transaction->getReturnUrl();
@@ -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);
}
@@ -12,6 +12,7 @@ use VRPaymentPayment\Core\Api\Transaction\Service\TransactionService;
use VRPaymentPayment\Core\Settings\Options\Integration;
use VRPaymentPayment\Core\Settings\Service\SettingsService;
use VRPaymentPayment\Core\Checkout\Struct\PaymentConfigStruct;
use VRPaymentPayment\Core\Util\LocaleCodeProvider;
use VRPayment\Sdk\Model\TransactionState;
/**
@@ -44,22 +45,30 @@ class PaymentIntegrationService
*/
private RouterInterface $router;
/**
* @var LocaleCodeProvider
*/
private LocaleCodeProvider $localeCodeProvider;
/**
* @param TransactionService $transactionService
* @param SettingsService $settingsService
* @param TransactionManagementService $transactionManagementService
* @param RouterInterface $router
* @param LocaleCodeProvider $localeCodeProvider
*/
public function __construct(
TransactionService $transactionService,
SettingsService $settingsService,
TransactionManagementService $transactionManagementService,
RouterInterface $router
RouterInterface $router,
LocaleCodeProvider $localeCodeProvider
) {
$this->transactionService = $transactionService;
$this->settingsService = $settingsService;
$this->transactionManagementService = $transactionManagementService;
$this->router = $router;
$this->localeCodeProvider = $localeCodeProvider;
}
/**
@@ -82,7 +91,9 @@ class PaymentIntegrationService
$transactionId
);
$javascriptUrl = $this->getTransactionJavaScriptUrl($settings, $transactionId);
$localeCode = $this->localeCodeProvider->getLocaleCodeFromContext($salesChannelContext->getContext());
$paymentPageLocale = $this->localeCodeProvider->mapToPaymentPageLocale($localeCode);
$javascriptUrl = $this->getTransactionJavaScriptUrl($settings, $transactionId, $paymentPageLocale);
$possiblePaymentMethods = $settings->getApiClient()
->getTransactionService()
@@ -161,9 +172,10 @@ class PaymentIntegrationService
*
* @param mixed $settings The plugin settings.
* @param int $transactionId The transaction ID.
* @param string $paymentPageLocale The payment page locale.
* @return string The absolute URL to the JavaScript component.
*/
private function getTransactionJavaScriptUrl($settings, int $transactionId): string
private function getTransactionJavaScriptUrl($settings, int $transactionId, string $paymentPageLocale = ''): string
{
$javascriptUrl = '';
switch ($settings->getIntegration()) {
@@ -176,6 +188,12 @@ class PaymentIntegrationService
->javascriptUrl($settings->getSpaceId(), $transactionId);
break;
}
if ($javascriptUrl && $paymentPageLocale) {
$separator = str_contains($javascriptUrl, '?') ? '&' : '?';
$javascriptUrl .= $separator . 'language=' . $paymentPageLocale;
}
return $javascriptUrl;
}
@@ -5,12 +5,8 @@ declare(strict_types=1);
namespace VRPaymentPayment\Core\Checkout\Service;
use Shopware\Core\Checkout\Payment\PaymentMethodCollection;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Core\Checkout\Cart\SalesChannel\CartService;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting;
use VRPaymentPayment\Core\Api\PaymentMethodConfiguration\Service\PaymentMethodConfigurationService;
use VRPaymentPayment\Core\Api\Transaction\Service\TransactionService;
use VRPaymentPayment\Core\Checkout\PaymentHandler\VRPaymentPaymentHandler;
@@ -50,12 +46,6 @@ class PaymentMethodFilterService
*/
private PaymentMethodUtil $paymentMethodUtil;
/**
* @var EntityRepository
* Repository for Shopware payment methods.
*/
private EntityRepository $paymentMethodRepository;
/**
* @var LoggerInterface
*/
@@ -77,15 +67,14 @@ class PaymentMethodFilterService
* @param TransactionService $transactionService
* @param PaymentMethodConfigurationService $paymentMethodConfigurationService
* @param PaymentMethodUtil $paymentMethodUtil
* @param EntityRepository $paymentMethodRepository
* @param TransactionManagementService $transactionManagementService
* @param CartService $cartService
*/
public function __construct(
SettingsService $settingsService,
TransactionService $transactionService,
PaymentMethodConfigurationService $paymentMethodConfigurationService,
PaymentMethodUtil $paymentMethodUtil,
EntityRepository $paymentMethodRepository,
TransactionManagementService $transactionManagementService,
CartService $cartService
) {
@@ -93,7 +82,6 @@ class PaymentMethodFilterService
$this->transactionService = $transactionService;
$this->paymentMethodConfigurationService = $paymentMethodConfigurationService;
$this->paymentMethodUtil = $paymentMethodUtil;
$this->paymentMethodRepository = $paymentMethodRepository;
$this->transactionManagementService = $transactionManagementService;
$this->cartService = $cartService;
}
@@ -208,8 +196,12 @@ class PaymentMethodFilterService
/**
* Builds a filtered PaymentMethodCollection based on allowed IDs.
*
* @param PaymentMethodCollection $paymentMethodCollection Original collection.
* @param string[] $allowedIds List of allowed configuration IDs.
* Filters the original collection (which already has Shopware's availability rules applied)
* to only include WhitelabelMachineName methods that are also allowed by the API.
* Non-WhitelabelMachineName methods are kept as-is.
*
* @param PaymentMethodCollection $paymentMethodCollection Original collection (already rule-filtered by Shopware).
* @param string[] $allowedIds List of allowed configuration IDs from the WhitelabelMachineName API.
* @param int $spaceId WhitelabelMachineName space ID.
* @param SalesChannelContext $salesChannelContext The context.
* @return PaymentMethodCollection The final collection.
@@ -220,21 +212,12 @@ class PaymentMethodFilterService
int $spaceId,
SalesChannelContext $salesChannelContext
): PaymentMethodCollection {
$paymentIds = [];
// Extract non-WhitelabelMachineName payment methods first.
foreach ($paymentMethodCollection as $paymentMethodCollectionItem) {
$isVRPaymentPM = VRPaymentPaymentHandler::class === $paymentMethodCollectionItem->getHandlerIdentifier();
if (!$isVRPaymentPM) {
$paymentIds[] = $paymentMethodCollectionItem->getId();
}
}
$allowedWLMethods = [];
// Fetch all WhitelabelMachineName payment method configurations for the space.
$paymentMethodConfigurations = $this->paymentMethodConfigurationService
->getAllPaymentMethodConfigurations($spaceId, $salesChannelContext->getContext());
// Check each configuration against the list of allowed IDs from WhitelabelMachineName API.
// Build a map of Shopware payment method ID => configuration for methods allowed by the API.
$allowedWLConfigByPmId = [];
foreach ($paymentMethodConfigurations as $paymentMethodConfiguration) {
if ($paymentMethodConfiguration->getPaymentMethod() === null) {
continue;
@@ -247,39 +230,31 @@ class PaymentMethodFilterService
$paymentMethodConfiguration->getSpaceId() === $spaceId
&& \in_array($pmConfigId, $allowedIds, true)
) {
$allowedWLMethods[] = $pmId;
$allowedWLConfigByPmId[$pmId] = $paymentMethodConfiguration;
}
}
// Combine non-WhitelabelMachineName and allowed WhitelabelMachineName payment methods.
$allPaymentIds = array_unique(array_merge($paymentIds, $allowedWLMethods));
// Filter the original collection to preserve Shopware's availability rule filtering.
// Non-WLM methods pass through unchanged; WLM methods are kept only if allowed by the API.
$collection = new PaymentMethodCollection();
foreach ($paymentMethodCollection as $method) {
$isVRPaymentPM = VRPaymentPaymentHandler::class === $method->getHandlerIdentifier();
if (!empty($allPaymentIds)) {
$criteria = new Criteria($allPaymentIds);
$criteria->addFilter(new EqualsFilter('active', true));
$criteria->addFilter(
new EqualsFilter('salesChannels.id', $salesChannelContext->getSalesChannelId())
);
$criteria->addSorting(new FieldSorting('position', FieldSorting::ASCENDING));
$criteria->addAssociation('media');
// Re-fetch the entities to ensure we have valid objects with all associations.
$result = $this->paymentMethodRepository->search($criteria, $salesChannelContext->getContext());
/** @var \Shopware\Core\Checkout\Payment\PaymentMethodEntity $method */
foreach ($result->getEntities() as $method) {
if (!$collection->has((string)$method->getId())) {
// Attach the configuration to the payment method as an extension for Twig access.
foreach ($paymentMethodConfigurations as $paymentMethodConfiguration) {
if ($paymentMethodConfiguration->getPaymentMethodId() === $method->getId()) {
$method->addExtension('vrpayment_config', $paymentMethodConfiguration);
break;
}
}
$collection->add($method);
}
if (!$isVRPaymentPM) {
$collection->add($method);
continue;
}
if (isset($allowedWLConfigByPmId[$method->getId()])) {
$method->addExtension('vrpayment_config', $allowedWLConfigByPmId[$method->getId()]);
$collection->add($method);
}
}
$collection->sort(function ($a, $b) {
return ($a->getPosition() ?? 0) <=> ($b->getPosition() ?? 0);
});
return $collection;
}
}
@@ -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)
@@ -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);
@@ -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'));
+1 -1
View File
@@ -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.2',
self::PLUGIN_SYSTEM_VERSION => '7.3.4',
];
}
+20
View File
@@ -91,6 +91,26 @@ class LocaleCodeProvider {
return $language->getLocale() ? $language->getLocale()->getCode() : $defaultLocale;
}
/**
* Maps a locale code to a VRPayment-supported payment page locale by matching the language prefix.
* E.g. de-CH -> de-DE, fr-CH -> fr-FR, en-US -> en-GB, it-CH -> it-IT.
*
* @param string $localeCode
* @return string
*/
public function mapToPaymentPageLocale(string $localeCode): string
{
$supportedLocales = [
'de' => self::LOCALE_GERMANY_GERMAN,
'fr' => self::LOCALE_FRANCE_FRENCH,
'it' => self::LOCALE_ITALY_ITALIAN,
'en' => self::LOCALE_GREAT_BRITAIN_ENGLISH,
];
$languagePrefix = substr($localeCode, 0, 2);
return $supportedLocales[$languagePrefix] ?? self::LOCALE_GREAT_BRITAIN_ENGLISH;
}
/**
* @param \Shopware\Core\Framework\Context $context
@@ -35,6 +35,7 @@
{% block vrpayment_settings_content_card_channel_config %}
<sw-sales-channel-config v-model:value="config"
v-model:selectedSalesChannelId="selectedSalesChannelId"
ref="configComponent"
:domain="CONFIG_DOMAIN">
@@ -45,14 +46,10 @@
<mt-card title="Sales Channel Switch">
{% block vrpayment_settings_content_card_channel_config_sales_channel_card_title %}
<sw-single-select
:value="selectedSalesChannelId"
:options="salesChannel.map(sc => ({ id: sc.id, name: sc.translated.name }))"
labelProperty="name"
valueProperty="id"
:isLoading="isLoading"
@update:value="onInput"
/>
<sw-sales-channel-switch
ref="channelSwitch"
@change-sales-channel-id="onSalesChannelSwitchChange($event, onInput)">
</sw-sales-channel-switch>
{% endblock %}
{% block vrpayment_settings_content_card_channel_config_sales_channel_card_footer %}
<template #footer>
@@ -41,6 +41,7 @@ Component.register('vrpayment-settings', {
isSetDefaultPaymentSuccessful: false,
isSettingDefaultPaymentMethods: false,
selectedSalesChannelId: null,
configIntegrationDefaultValue: 'payment_page',
configEmailEnabledDefaultValue: true,
@@ -70,7 +71,7 @@ Component.register('vrpayment-settings', {
config: {
handler(configData) {
const defaultConfig = (this.$refs.configComponent.allConfigs || {}).null || {};
const salesChannelId = this.$refs.configComponent.selectedSalesChannelId;
const salesChannelId = this.selectedSalesChannelId;
if (salesChannelId === null) {
this.applicationKeyFilled = !!this.config[this.CONFIG_APPLICATION_KEY];
@@ -137,6 +138,16 @@ Component.register('vrpayment-settings', {
this.$emit('update:value', configData);
},
deep: true
},
selectedSalesChannelId: {
handler(newValue) {
this.$nextTick(() => {
if (this.$refs.channelSwitch) {
this.$refs.channelSwitch.salesChannelId = newValue || '';
}
});
}
}
},
@@ -198,7 +209,7 @@ Component.register('vrpayment-settings', {
},
async validateHeadlessIntegration() {
const salesChannelId = this.$refs.configComponent.selectedSalesChannelId;
const salesChannelId = this.selectedSalesChannelId;
const currentIntegration = this.config[this.CONFIG_INTEGRATION];
// If integration is 'payment_page', it is always valid.
@@ -270,7 +281,7 @@ Component.register('vrpayment-settings', {
return false;
}
this.VRPaymentConfigurationService.registerWebHooks(this.$refs.configComponent.selectedSalesChannelId)
this.VRPaymentConfigurationService.registerWebHooks(this.selectedSalesChannelId)
.then(() => {
this.createNotificationSuccess({
title: this.$tc('vrpayment-settings.settingForm.titleSuccess'),
@@ -291,7 +302,7 @@ Component.register('vrpayment-settings', {
return false;
}
this.VRPaymentConfigurationService.synchronizePaymentMethodConfiguration(this.$refs.configComponent.selectedSalesChannelId)
this.VRPaymentConfigurationService.synchronizePaymentMethodConfiguration(this.selectedSalesChannelId)
.then(() => {
this.createNotificationSuccess({
title: this.$tc('vrpayment-settings.settingForm.titleSuccess'),
@@ -328,7 +339,7 @@ Component.register('vrpayment-settings', {
onSetPaymentMethodDefault() {
this.isSettingDefaultPaymentMethods = true;
this.VRPaymentConfigurationService.setVRPaymentAsSalesChannelPaymentDefault(
this.$refs.configComponent.selectedSalesChannelId
this.selectedSalesChannelId
).then(() => {
this.isSettingDefaultPaymentMethods = false;
this.isSetDefaultPaymentSuccessful = true;
@@ -385,6 +396,13 @@ Component.register('vrpayment-settings', {
});
this.isTesting = false;
});
},
onSalesChannelSwitchChange(id, onInput) {
this.selectedSalesChannelId = id;
if (typeof onInput === 'function') {
onInput(id);
}
}
}
});
File diff suppressed because one or more lines are too long
@@ -17,7 +17,6 @@
<argument type="service" id="VRPaymentPayment\Core\Api\Transaction\Service\TransactionService"/>
<argument type="service" id="VRPaymentPayment\Core\Api\PaymentMethodConfiguration\Service\PaymentMethodConfigurationService"/>
<argument type="service" id="VRPaymentPayment\Core\Util\PaymentMethodUtil"/>
<argument type="service" id="payment_method.repository"/>
<argument type="service" id="VRPaymentPayment\Core\Checkout\Service\TransactionManagementService"/>
<argument type="service" id="Shopware\Core\Checkout\Cart\SalesChannel\CartService"/>
<call method="setLogger">
@@ -30,6 +29,7 @@
<argument type="service" id="VRPaymentPayment\Core\Settings\Service\SettingsService"/>
<argument type="service" id="VRPaymentPayment\Core\Checkout\Service\TransactionManagementService"/>
<argument type="service" id="router"/>
<argument type="service" id="VRPaymentPayment\Core\Util\LocaleCodeProvider"/>
</service>
<service id="VRPaymentPayment\Core\Checkout\Service\CartRecoveryService">
@@ -18,6 +18,7 @@
<service id="VRPaymentPayment\Core\Checkout\StoreApi\Route\CartRecoveryRoute">
<argument type="service" id="VRPaymentPayment\Core\Checkout\Service\CartRecoveryService"/>
<argument type="service" id="VRPaymentPayment\Core\Api\Transaction\Service\TransactionService"/>
</service>
<service id="VRPaymentPayment\Core\Checkout\StoreApi\Route\InvoiceRoute">
@@ -7,7 +7,7 @@
],
"dynamic": [],
"js": [
"/bundles/vrpaymentpayment/administration/assets/v-r-payment-payment-D_mm2NL2.js"
"/bundles/vrpaymentpayment/administration/assets/v-r-payment-payment-CPfpiGQp.js"
],
"legacy": false,
"preload": []
@@ -1,6 +1,6 @@
{
"main.js": {
"file": "assets/v-r-payment-payment-D_mm2NL2.js",
"file": "assets/v-r-payment-payment-CPfpiGQp.js",
"name": "v-r-payment-payment",
"src": "main.js",
"isEntry": true,
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long