Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG_de-DE.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# 6.4.1
* Neue Konfigurationsoption für automatischen Einzug / Refund bei Statuswechsel

# 6.4.0
* Unzer Direkt-Überweisung als neue Zahlungsmethode hinzugefügt
* Update PHP-SDK
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG_en-GB.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# 6.4.1
* New config setting to capture and refund on status change

# 6.4.0
* Unzer Direct Bank Transfer added as new payment method
* Update PHP-SDK
Expand Down
56 changes: 12 additions & 44 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,57 +1,35 @@
# Unzer Payment plugin for Shopware 6
# UnzerPayment

Use Unzer Payment plugin for Shopware 6 to provide an easy-to-install payment gateway integration for all your online payments.

## Description

Accept payments with cards, bank transfers, wallets, and other global and local payment methods. Unzer Payment plugin helps you with quick and easy integration, full support, and flexible solutions that grow with your business. We are your payment partner for every situation.

## Features

* Seamless integration into the Shop-system
* Merchant-friendly order management with up-to-date payment details, real-time billing and refunds made easy.
* Payment processing via the Unzer Payment API
* 3D-Secure authentication
* PCI-DSS Level 1 certified

## Content security policy (CSP)

If you are using a Content Security Policy (CSP) you must include different Unzer URL's to your policy, which are required by the UI components to work. For more information, please go to [Unzer Documentation CSP Information](https://docs.unzer.com/online-payments/ui-component-v2/#content-security-policy-csp).

## Supported payment methods

Unzer payment integration for Shopware 6 includes the following payment methods:
Unzer payment integration for Shopware 6 including the following payment methods:
* Alipay
* Apple Pay
* Bancontact
* Credit Card
* Unzer Direct Bank Transfer
* Direct Bank Transfer
* EPS
* Google Pay
* iDEAL
* Invoice
* PayPal
* Prepayment
* SEPA Direct Debit
* SEPA direct debit
* SOFORT
* TWINT
* Unzer Direct Debit
* Unzer direct Debit (secured)
* Unzer Direct
* Unzer direct debit (secured)
* Unzer Invoice B2C / B2B (secured)
* Unzer Installment (secured)
* WeChat Pay

Regarding plugin compatibility, please take a look at the release notes for more information.
Regarding plugin compatibility, please take a look at the [Shopware store page](https://store.shopware.com/en/unzer48059319318f/unzer-payments-for-shopware-6.html).

## Installation

### For production

1. Upload the plugin files into the `custom/plugins` folder in your shopware installation.
2. Inside the plugin directory `custom/plugins/UnzerPayment6` run `composer install --no-dev`
3. Switch to admin and install the plugin using the Shopware plugin manager and configure it as you need.

### For development

1. Clone the plugin repository into the `custom/plugins` folder in your shopware installation.
2. Inside the plugin directory run `composer install`
3. Go to the plugin manager and install/activate the plugin.
Expand All @@ -62,18 +40,8 @@ Regarding plugin compatibility, please take a look at the release notes for more

This will automatically generate all files required for the plugin to work correctly

## User Guide

Please find information on installation, configuration, usage etc on our [documentation pages](https://docs.unzer.com/plugins/shopware-6).

## Support

For any issues or questions please get in touch with our support.

**Email**: support@unzer.com

**Phone**: +49 (0)6221/6471-100

**Twitter**: [@UnzerTech](https://twitter.com/UnzerTech)
## Configuration
After the actual plugin installation it is necessary to add the new payment methods to the desired sales channel.
Currently, the only sales channel that is supported is the Storefront.

**Webpage**: https://unzer.com/
Further information and configuration you can find within the <a href="https://docs.unzer.com/plugins/shopware-6/" target="_blank">manual</a>
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "unzerdev/shopware6",
"description": "Unzer payment integration for Shopware 6",
"version": "6.4.0",
"version": "6.4.1",
"type": "shopware-platform-plugin",
"license": "Apache-2.0",
"minimum-stability": "dev",
Expand Down
15 changes: 10 additions & 5 deletions src/Components/CancelService/CancelService.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace UnzerPayment6\Components\CancelService;

use Psr\Log\LoggerInterface;
use Shopware\Core\Checkout\Cart\Tax\Struct\CalculatedTax;
use Shopware\Core\Checkout\Order\Aggregate\OrderTransaction\OrderTransactionEntity;
use Shopware\Core\Checkout\Payment\PaymentException;
Expand All @@ -28,13 +29,16 @@ class CancelService implements CancelServiceInterface
private EntityRepository $orderTransactionRepository;

private ClientFactoryInterface $clientFactory;
private LoggerInterface $logger;

public function __construct(
EntityRepository $orderTransactionRepository,
ClientFactoryInterface $clientFactory
ClientFactoryInterface $clientFactory,
LoggerInterface $logger
) {
$this->orderTransactionRepository = $orderTransactionRepository;
$this->clientFactory = $clientFactory;
$this->logger = $logger;
}

/**
Expand Down Expand Up @@ -98,7 +102,7 @@ public function cancelChargeById(string $orderTransactionId, string $chargeId, f
/**
* {@inheritdoc}
*/
public function cancelAuthorizationById(string $orderTransactionId, string $authorizationId, float $amountGross, Context $context): void
public function cancelAuthorizationById(string $orderTransactionId, string $paymentId, float $amountGross, Context $context): void
{
$transaction = $this->getOrderTransaction($orderTransactionId, $context);

Expand All @@ -107,14 +111,15 @@ public function cancelAuthorizationById(string $orderTransactionId, string $auth
}

$client = $this->clientFactory->createClient(KeyPairContext::createFromOrderTransaction($transaction));
$authorization = $client->fetchAuthorization($paymentId);

if ($this->isPaylaterPaymentMethod($transaction->getPaymentMethodId())) {
$client->cancelAuthorizedPayment($authorizationId, new Cancellation($amountGross));
$this->logger->info('Canceling authorization by payment', ['authorization' => $authorization->getPayment()]);
$client->cancelAuthorizedPayment($authorization->getPayment(), new Cancellation($amountGross));

return;
}

$authorization = $client->fetchAuthorization($authorizationId);
$this->logger->info('Canceling authorization', ['authorization' => $authorization]);
$authorization->cancel($amountGross);
}

Expand Down
6 changes: 3 additions & 3 deletions src/Components/CancelService/CancelServiceInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ public function cancelChargeById(
* @throws RuntimeException
*/
public function cancelAuthorizationById(
string $orderTransactionId,
string $authorizationId,
float $amountGross,
string $orderTransactionId,
string $paymentId,
float $amountGross,
Context $context
): void;
}
26 changes: 13 additions & 13 deletions src/Components/ClientFactory/ClientFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,42 +21,42 @@ class ClientFactory implements ClientFactoryInterface

public function __construct(ConfigReaderInterface $configReader, DebugHandlerInterface $debugHandler, KeyPairConfigReader $keyPairConfigReader)
{
$this->configReader = $configReader;
$this->debugHandler = $debugHandler;
$this->configReader = $configReader;
$this->debugHandler = $debugHandler;
$this->keyPairConfigReader = $keyPairConfigReader;
}

public function createClient(KeyPairContext $keyPairContext, string $locale = self::DEFAULT_LOCALE): Unzer
{
$config = $this->configReader->read($keyPairContext->getSalesChannelId());

$client = new Unzer($this->keyPairConfigReader->getPrivateKey($keyPairContext), $locale);
$client->setDebugMode((bool) $config->get(ConfigReader::CONFIG_KEY_EXTENDED_LOGGING));
$client->setDebugHandler($this->debugHandler);
$this->applyGlobalClientSettings($client, $keyPairContext->getSalesChannelId());

return $client;
}

public function createClientFromPrivateKey(string $privateKey, string $salesChannelId = '', string $locale = self::DEFAULT_LOCALE): Unzer
{
$config = $this->configReader->read($salesChannelId);

$client = new Unzer($privateKey, $locale);
$client->setDebugMode((bool) $config->get(ConfigReader::CONFIG_KEY_EXTENDED_LOGGING));
$client->setDebugHandler($this->debugHandler);
$this->applyGlobalClientSettings($client, $salesChannelId);

return $client;
}

public function createClientFromPublicKey(string $publicKey, string $salesChannelId = '', string $locale = self::DEFAULT_LOCALE): Unzer
{
$config = $this->configReader->read($salesChannelId);
$privateKey = $this->keyPairConfigReader->getMatchingKey($publicKey, $salesChannelId);

$client = new Unzer($privateKey, $locale);
$client->setDebugMode((bool) $config->get(ConfigReader::CONFIG_KEY_EXTENDED_LOGGING));
$client->setDebugHandler($this->debugHandler);
$this->applyGlobalClientSettings($client, $salesChannelId);

return $client;
}

protected function applyGlobalClientSettings(Unzer $client, string $salesChannelId = '')
{
$config = $this->configReader->read($salesChannelId);
$client->setDebugMode((bool)$config->get(ConfigReader::CONFIG_KEY_EXTENDED_LOGGING));
$client->setDebugHandler($this->debugHandler);
$client->setClientIp($_SERVER['REMOTE_ADDR'] ?? null);
}
}
2 changes: 2 additions & 0 deletions src/Components/ConfigReader/ConfigReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ class ConfigReader implements ConfigReaderInterface
public const CONFIG_KEY_GOOGLE_PAY_BUTTON_SIZE_MODE = 'googlePayButtonSizeMode';

public const CONFIG_KEY_PAYPAL_SHOW_SAVE_ACCOUNT = 'paypalShowSaveAccount';
public const CONFIG_KEY_DELIVERY_STATUS_FOR_CAPTURE = 'deliveryStatusForAutomaticCapture';
public const CONFIG_KEY_DELIVERY_STATUS_FOR_REFUND = 'deliveryStatusForAutomaticRefund';

private SystemConfigService $systemConfigService;

Expand Down
137 changes: 137 additions & 0 deletions src/Components/UnzerUtil/UnzerTransactionUtil.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<?php

namespace UnzerPayment6\Components\UnzerUtil;

use Exception;
use Psr\Log\LoggerInterface;
use Shopware\Core\Checkout\Order\Aggregate\OrderTransaction\OrderTransactionEntity;
use Shopware\Core\Checkout\Order\OrderEntity;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use UnzerPayment6\Components\CancelService\CancelServiceInterface;
use UnzerPayment6\Components\ClientFactory\ClientFactoryInterface;
use UnzerPayment6\Components\Struct\KeyPairContext;
use UnzerPayment6\Components\TransactionStateHandler\TransactionStateHandlerInterface;
use UnzerPayment6\Installer\PaymentInstaller;
use UnzerSDK\Exceptions\UnzerApiException;
use UnzerSDK\Resources\TransactionTypes\Charge;

class UnzerTransactionUtil
{

public function __construct(
protected readonly EntityRepository $orderTransactionRepository,
protected readonly ClientFactoryInterface $clientFactory,
protected readonly TransactionStateHandlerInterface $transactionStateHandler,
protected readonly CancelServiceInterface $cancelService,
protected readonly LoggerInterface $logger
)
{
}

/**
* Gets the order transaction for the provided order entity. Only Unzer transactions are considered.
*/
public function getOrderTransactionFromOrder(OrderEntity $orderEntity, Context $context): ?OrderTransactionEntity
{
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('orderId', $orderEntity->getId()));
$criteria->addFilter(new EqualsAnyFilter('paymentMethodId', PaymentInstaller::PAYMENT_METHOD_IDS));
$criteria->addAssociations([
'order',
'order.billingAddress',
'order.currency',
'order.documents',
'order.documents.documentType',
'paymentMethod',
]);
return $this->orderTransactionRepository->search($criteria, $context)->first();
}

/**
* @throws Exception
*/
public function captureOrder(OrderEntity $order, Context $context): bool
{
$this->logger->info('Capturing order', ['order' => $order->getId()]);
$orderTransaction = $this->getOrderTransactionFromOrder($order, $context);

if ($orderTransaction === null) {
return false;
}

$client = $this->clientFactory->createClient(KeyPairContext::createFromOrderTransaction($orderTransaction));
try {
$charge = $client->performChargeOnPayment($orderTransaction->getId(), new Charge($orderTransaction->getAmount()->getTotalPrice()));
$this->transactionStateHandler->transformTransactionState(
$orderTransaction->getId(),
$charge->getPayment(),
$context
);
} catch (UnzerApiException $e) {
throw new Exception($e->getMerchantMessage() ?: $e->getClientMessage());
}

return true;
}

public function refundOrder(OrderEntity $order, Context $context)
{
$this->logger->info('Refunding order', ['order' => $order->getId()]);
$orderTransaction = $this->getOrderTransactionFromOrder($order, $context);

if ($orderTransaction === null) {
return false;
}

$client = $this->clientFactory->createClient(KeyPairContext::createFromOrderTransaction($orderTransaction));
try {
$payment = $client->fetchPayment($orderTransaction->getId());
foreach ($payment->getCharges() as $charge) {
try {
if ($charge->isError()) {
continue;
}
$this->logger->info('Refunding charge', ['chargeId' => $charge->getId()]);
$this->cancelService->cancelChargeById(
$orderTransaction->getId(),
$charge->getId(),
$charge->getAmount()-$charge->getCancelledAmount(),
null,
$context
);
} catch (\Throwable $e) {
$this->logger->error('Error while refunding charge', ['charge' => $charge->getId(), 'error' => $e->getMessage()]);
}
}
$authorization = $payment->getAuthorization();
if ($authorization !== null && !$authorization->isError()) {
try {
$this->logger->info('Refunding authorization', ['paymentId' => $payment->getId(), 'authorizationId' => $authorization->getId()]);

$this->cancelService->cancelAuthorizationById(
$orderTransaction->getId(),
$payment->getId(),
$authorization->getAmount() - $authorization->getCancelledAmount(),
$context
);
} catch (\Throwable $e) {
$this->logger->error('Error while refunding authorization', ['authorization' => $authorization->getId(), 'error' => $e->getMessage()]);
}
}
$this->transactionStateHandler->transformTransactionState(
$orderTransaction->getId(),
$payment,
$context
);
} catch (UnzerApiException $e) {
throw new Exception($e->getMerchantMessage() ?: $e->getClientMessage());
}

}


}
2 changes: 2 additions & 0 deletions src/DependencyInjection/event_listeners.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
<argument type="service" id="unzer_payment.logger"/>
<argument type="service" id="event_dispatcher"/>
<argument type="service" id="UnzerPayment6\Components\ShipService\ShipService"/>
<argument type="service" id="UnzerPayment6\Components\ConfigReader\ConfigReader"/>
<argument type="service" id="UnzerPayment6\Components\UnzerUtil\UnzerTransactionUtil"/>

<tag name="kernel.event_subscriber"/>
</service>
Expand Down
Loading