diff --git a/guides/plugins/apps/payment.md b/guides/plugins/apps/payment.md index aff11cca7..b098bca38 100644 --- a/guides/plugins/apps/payment.md +++ b/guides/plugins/apps/payment.md @@ -40,6 +40,10 @@ Depending on the URLs you provide, Shopware knows which kind of payment flow you ## Synchronous payments +::: info +Be aware, that from Shopware 6.7.0.0 onwards your app-server **has to** respond with a payment state in its response, if you intend to change the transaction state. +::: + There are different types of payments. Synchronous payment is the simplest of all and does not need any additional interaction with the customer. If you have defined a `pay-url`, you can choose to be informed about and possibly process the payment or not. @@ -55,7 +59,7 @@ Below you can see an example of a simple answer from your app to mark a payment Request content is JSON -```json +```json5 { "source": { "url": "http:\/\/localhost:8000", @@ -73,7 +77,7 @@ Request content is JSON Refer to an example on [payment payload](https://github.com/shopware/app-php-sdk/blob/main/tests/Context/_fixtures/payment.json) and the response should look like this: -```json +```json5 { "status": "authorize" } @@ -82,7 +86,7 @@ Refer to an example on [payment payload](https://github.com/shopware/app-php-sdk Refer to possible [status values](#all-possible-payment-states). Failing states can also have a `message` property with the reason, which will be logged and could be seen as information for the merchant. -```json +```json5 { "status": "authorize", "message": "The customer failed to pass the credit check." @@ -147,6 +151,10 @@ class PaymentController { ## Asynchronous payments +::: info +Be aware, that from Shopware 6.7.0.0 onwards your app-server **has to** respond with a payment state in its response, if you intend to change the transaction state. +::: + Asynchronous payments are more complicated than synchronous payments. They require interaction with the customer and a redirect to the payment provider, such as PayPal or Stripe. @@ -159,7 +167,7 @@ Here is how it works: * The browser will be redirected to this URL and processes his order, and the payment provider will redirect the customer back to the `returnUrl` provided in the first request. * Shopware sends a second `POST` request to the `finalize-url` with the `orderTransaction` and all the query parameters passed by the payment provider to Shopware. -* Your app server responds with a `status` and a `message` if necessary, like in the synchronous payment. +* Your app server responds with a `status` and, if necessary a `message`, like in the synchronous payment. @@ -167,7 +175,7 @@ Here is how it works: Request content is JSON -```json +```json5 { "source": { "url": "http:\/\/localhost:8000", @@ -188,7 +196,7 @@ You can find an example refund payload [here](https://github.com/shopware/app-ph and your response should look like this: -```json +```json5 { "redirectUrl": "https://payment.app/customer/gotoPaymentProvider" } @@ -267,7 +275,7 @@ The response `status` value determines the outcome of the payment, e.g.: Request content is JSON -```json +```json5 { "source": { "url": "http:\/\/localhost:8000", @@ -285,7 +293,7 @@ Request content is JSON and your response should look like this: -```json +```json5 { "status": "paid" } @@ -294,7 +302,7 @@ and your response should look like this: Refer possible [status values](#all-possible-payment-states). Failing states can also have a `message` property with the reason, which will be logged and could be seen as information for the merchant. -```json +```json5 { "status": "authorize", "message": "The customer failed to pass the credit check." @@ -366,23 +374,27 @@ Once you add specific parameters to the order placement request in the Storefron This enables your payment handler to capture the payment successfully when the order is placed. For this, you have two calls available during the order placement, the `validate` call to verify, -that the payment reference is valid and if not, stop the placement of the order, and the `capture` call, +that the payment reference is valid and if not, stop the placement of the order, and the `pay` call, which then allows the payment to be processed to completion after the order has been placed and persisted. Let's first talk about the `validate` call. Here, you will receive three items to validate your payment. The `cart` with all its line items, the `requestData` from the `CartOrderRoute` request and the current `salesChannelContext`. This allows you to validate, if the payment reference you may have given your payment handler via the Storefront implementation is valid and will be able to be used to pay the order which is about to be placed. -The array data you may send as the `preOrderPayment` object in your response will be forwarded to your `capture` call, so you don't have to worry about identifying the order by looking at the cart from the `validate` call. +The array data you may send as the `preOrderPayment` object in your response will be forwarded to your `pay` call, so you don't have to worry about identifying the order by looking at the cart from the `validate` call. If the payment is invalid, either return a response with an error response code or provide a `message` in your response. +::: info +Be aware, that from Shopware 6.7.0.0 onwards your app-server **has to** respond with a payment state in its response, if you intend to change the transaction state. +::: + Request content is JSON -```json +```json5 { "source": { "url": "http:\/\/localhost:8000", @@ -403,7 +415,7 @@ Request content is JSON You can refer to an example on [validation payload](https://github.com/shopware/app-php-sdk/blob/main/tests/Context/_fixtures/payment-validation.json) and your response looks like this: -```json +```json5 { "preOrderPayment": { "myCustomReference": "1234567890" @@ -411,7 +423,7 @@ You can refer to an example on [validation payload](https://github.com/shopware/ } ``` -this will be forwarded to the `capture` call afterward. +this will be forwarded to the `pay` call afterward. @@ -468,7 +480,7 @@ class PaymentController { -If the payment has been validated and the order has been placed, you then receive another call to your `capture` endpoint. +If the payment has been validated and the order has been placed, you then receive another call to your `pay` endpoint. You will receive the `order`, the `orderTransaction` and also the `preOrderPayment` array data, that you have sent in your validate call. @@ -477,7 +489,7 @@ You will receive the `order`, the `orderTransaction` and also the `preOrderPayme Request content is JSON -```json +```json5 { "source": { "url": "http:\/\/localhost:8000", @@ -498,7 +510,7 @@ Request content is JSON and your response should look like this: -```json +```json5 { "status": "authorize" } @@ -507,7 +519,7 @@ and your response should look like this: You can find all possible status values [here](#all-possible-payment-states). Failing states can also have a `message` property with the reason, which will be logged and could be seen as information for the merchant. -```json +```json5 { "status": "authorize", "message": "The customer failed to pass the credit check." @@ -525,7 +537,7 @@ use Shopware\App\SDK\Shop\ShopResolver; use Shopware\App\SDK\Context\ContextResolver; use Shopware\App\SDK\Response\PaymentResponse; -function capture(RequestInterface $request): ResponseInterface +function pay(RequestInterface $request): ResponseInterface { // injected or build by yourself $shopResolver = new ShopResolver($repository); @@ -589,13 +601,17 @@ Your app will need to register captured amounts and create and persist a refund Similar to the other requests, on your `refund` call you will receive the data required to process your refund. This is the `order` with all its details and also the `refund` which holds the information on the `amount`, the referenced `capture` and, if provided, a `reason` and specific `positions` which items are being refunded. +::: info +Be aware, that from Shopware 6.7.0.0 onwards your app-server **has to** respond with a payment state in its response, if you intend to change the transaction state. +::: + Request content is JSON -```json +```json5 { "source": { "url": "http:\/\/localhost:8000", @@ -613,7 +629,7 @@ Request content is JSON You can refer to [refund payload](https://github.com/shopware/app-php-sdk/blob/main/tests/Context/_fixtures/refund.json) example and your response should look like this: -```json +```json5 { "status": "completed" } @@ -663,13 +679,17 @@ At this point, a valid running billing agreement between the customer and the PS Use any of the other payment flows to capture the initial order and create such an agreement during the checkout. Afterward, this flow can capture the payment for every recurring payment order. +::: info +Be aware, that from Shopware 6.7.0.0 onwards your app-server **has to** respond with a payment state in its response, if you intend to change the transaction state. +::: + Request content is JSON -```json +```json5 { "source": { "url": "http:\/\/localhost:8000", @@ -687,7 +707,7 @@ Request content is JSON You can refer to an example on [recurring capture payload](https://github.com/shopware/app-php-sdk/blob/main/tests/Context/_fixtures/payment.json) and your response looks like this: -```json +```json5 { "status": "paid" } diff --git a/guides/plugins/plugins/checkout/payment/add-payment-plugin.md b/guides/plugins/plugins/checkout/payment/add-payment-plugin.md index 725713241..26c6a8ca5 100644 --- a/guides/plugins/plugins/checkout/payment/add-payment-plugin.md +++ b/guides/plugins/plugins/checkout/payment/add-payment-plugin.md @@ -17,49 +17,33 @@ Check out the new documentation here: [Add Payment Plugin (>6.7)](/docs/v6.7/gui ## Overview -Payments are an essential part of the checkout process. That's why Shopware 6 offers an easy platform on which you can build payment plugins. +Payments are an essential part of the checkout process. +That's why Shopware 6 offers an easy platform on which you can build payment plugins. ## Prerequisites -The examples mentioned in this guide are built upon our [Plugin base guide](../../plugin-base-guide). +The examples mentioned in this guide are built upon our plugin base guide. -If you want to understand the payment process in detail, head to our [Payment Concept](../../../../../concepts/commerce/checkout-concept/payments). + -::: info -Refer to this video on **[Introduction to payment handlers](https://www.youtube.com/watch?v=K58--Pxvudk)** that details you about payment extensions and payment handlers. Also available on our free online training ["Shopware 6 Backend Development"](https://academy.shopware.com/courses/shopware-6-backend-development-with-jisse-reitsma). -::: +If you want to understand the payment process in detail, head to our Payment Concept. + + ## Creating a custom payment handler To create a payment method with your plugin, you have to add a custom payment handler. -You can create your payment handler by implementing one of the following interfaces: - -| Interface | DI container tag | Usage | -|:------------------------------------|:------------------------------------|:---------------------------------------------------------------------------------------------------| -| SynchronousPaymentHandlerInterface | `shopware.payment.method.sync` | Payment can be handled locally, e.g. pre-payment | -| AsynchronousPaymentHandlerInterface | `shopware.payment.method.async` | A redirect to an external payment provider is required, e.g. PayPal | -| PreparedPaymentHandlerInterface | `shopware.payment.method.prepared` | The payment was prepared beforehand and will only be validated and captured by your implementation | -| RefundPaymentHandlerInterface | `shopware.payment.method.refund` | The payment allows refund handling | -| RecurringPaymentHandlerInterface | `shopware.payment.method.recurring` | The payment allows recurring payments, e.g. subscriptions | - -Depending on the interface, those methods are required: - -* `pay`: This method will be called after an order has been placed. You receive a `Shopware\Core\Checkout\Payment\Cart\AsyncPaymentTransactionStruct` or a `Shopware\Core\Checkout\Payment\Cart\SyncPaymentTransactionStruct` which contains the transactionId, order details, the amount of the transaction, a return URL, payment method information and language information. Please be aware, Shopware 6 supports multiple transactions, and you have to use the amount provided and not the total order amount. If you're using the `AsynchronousPaymentHandlerInterface`, the `pay` method has to return a `RedirectResponse` to redirect the customer to an external payment provider. Note: The [AsyncPaymentTransactionStruct](https://github.com/shopware/shopware/blob/v6.3.4.1/src/Core/Checkout/Payment/Cart/AsyncPaymentTransactionStruct.php) contains a return URL. This represents the URL the external payment provider needs to know to redirect your customer back to your shop. If an error occurs while, e.g., calling the API of your external payment provider, you should throw an `AsyncPaymentProcessException`. Shopware 6 will handle this exception and set the transaction to the `cancelled` state. The same happens if you use the `SynchronousPaymentHandlerInterface`: throw a `SyncPaymentProcessException` in an error case. -* `finalize`: The `finalize` method is only required if you implemented the `AsynchronousPaymentHandlerInterface`, returned a `RedirectResponse` in your `pay` method, and the customer has been redirected from the payment provider back to Shopware 6. You must check here if the payment was successful and update the order transaction state accordingly. Similar to the pay action, you can throw exceptions if some error cases occur. Throw the `CustomerCanceledAsyncPaymentException` if the customer canceled the payment process on the payment provider site. If another general error occurs, throw the `AsyncPaymentFinalizeException` e.g., if your call to the payment provider API fails. Shopware 6 will handle these exceptions and set the transaction to the `cancelled` state. -* `validate`: This method will be called before an order was placed and should check if a given prepared payment is valid. The payment handler has to verify the given payload with the payment service because Shopware cannot ensure that the transaction created by the frontend is valid for the current cart. Throw a `ValidatePreparedPaymentException` to fail the validation in your implementation. -* `capture`: This method will be called after an order was placed, but only if the validation did not fail and stop the payment flow before. At this point, the order was created, and the payment handler will be called again to charge the payment. When the charge was successful, the payment handler should update the transaction state to `paid`. The user will be forwarded to the finish page. Throw a `CapturePreparedPaymentException` on any errors to fail the capture process and, the after-order process will be active so that the customer can complete the payment again. -* `refund`: This method is called whenever a successful transaction is claimed to be refunded. The implementation of the refund handler should validate the legitimacy of the refund and call the PSP to refund the given transaction. Throw a `RefundException` to let the refund fail. -* `captureRecurring`: This method is called whenever a recurring payment is charged. At this point, a valid billing agreement with the payment provider should exist. Use some of the other payment methods for handling the initial order and billing agreement. Use this interface only for handling all recurring captures afterward. - -All payment handler methods have the `\Shopware\Core\System\SalesChannel\SalesChannelContext` injected, except for the new `captureRecurring` method. Note that this class contains nullable properties. If you want to use this information, you must ensure in your code that they are set and not `NULL`. +Shopware provides you with a handy `Shopware\Core\Checkout\Payment\Cart\PaymentHandler\AbstractPaymentHandler` abstract class for you to extend to get you started quickly. ### Registering the service -Before we're going to have a look at some examples, we need to register our new service to the [Dependency Injection](../../plugin-fundamentals/dependency-injection) container. We'll use a class called `ExamplePayment` here. +Before we're going to have a look at some examples, we need to register our new service to the [Dependency Injection](../../plugin-fundamentals/dependency-injection) container. +Please make sure to add the `shopware.payment.method` tag to your service definition, otherwise Shopware won't recognize your service as a payment handler. -```xml -// /src/Resources/config/services.xml +We'll use a class called `MyCustomPaymentHandler` here. + +```xml [/src/Resources/config/services.xml] - - - - - - + + ``` -We inject the `OrderTransactionStateHandler` in this example, as it helps change an order's transaction state, e.g. to `paid`. The payment handler has to be marked as such as well; hence the tag `shopware.payment.method.sync`, `shopware.payment.method.async` or `shopware.payment.method.prepared` respectively for a synchronous, an asynchronous or a prepared payment handler. - Now, let's start with the actual examples. -### Synchronous example +### Example payment handlers + + -The following will be a synchronous example, so that no redirect will happen, and the payment can be handled in the shop. Therefore, you don't have to return a `RedirectResponse` in the `pay` method; no `finalize` method is necessary either. + -Therefore, changing the `stateId` of the order should already be done in the `pay` method since there will be no `finalize` method. If you have to execute some logic that might fail, e.g., a call to an external API, you should throw a `SyncPaymentProcessException`. Shopware 6 will handle this exception and set the transaction to the `cancelled` state. +The following will be a synchronous example, so that no redirect will happen, and the payment can be handled in the shop. +Therefore, you don't have to return a `RedirectResponse` in the `pay` method; no `finalize` method is necessary either. -```php -// /src/Service/ExamplePayment.php +Therefore, changing the `stateId` of the order should already be done in the `pay` method since there will be no `finalize` method. +If you have to execute some logic that might fail, e.g., a call to an external API, you should throw a `PaymentException`. +Shopware 6 will handle this exception and set the transaction to the `failed` state. + +::: code-group + +```php [MyCustomPaymentHandler.php] transactionStateHandler = $transactionStateHandler; + // This payment handler does not support recurring payments nor refunds + return false; } - public function pay(SyncPaymentTransactionStruct $transaction, RequestDataBag $dataBag, SalesChannelContext $salesChannelContext): void + /** + * This method is always called during the checkout. + * You should process the payment here and return a RedirectResponse if the payment process requires an asynchronous approach. + * In that case, the finalize method will be called additionally during checkout after the redirect. + * If the payment process is synchronous, you should return null. + */ + public function pay(Request $request, PaymentTransactionStruct $transaction, Context $context, ?Struct $validateStruct): ?RedirectResponse { - $context = $salesChannelContext->getContext(); - $this->transactionStateHandler->paid($transaction->getOrderTransaction()->getId(), $context); + // In here you should probably call your payment provider to precess the payment + // $this->myPaymentProvider->processPayment($transaction); + + // afterward you should update the transaction with the new state + $this->transactionStateHandler->process($transaction->getOrderTransactionId(), $context); + + return null; } } ``` -All it does now is to set the state of the order transaction to `paid`. +```xml [services.xml] + + + + + + + + + + + +``` + +::: + +This payment handler does not do a lot in the current state but is a good starting point for your custom payment handler. -### Asynchronous example + -In the asynchronous example, the customer gets redirected to an external payment provider, which then, in return, has to redirect your customer back to your shop. Therefore, you must first redirect your customer to the payment provider by returning a `RedirectResponse`. + -Also, you need a `finalize` method to properly handle your customer when he was returned to your shop. This is where you check the payment state and set the order transaction state accordingly. +In the asynchronous example, the customer gets redirected to an external payment provider, +which then, in return, has to redirect your customer back to your shop. +Therefore, you must first redirect your customer to the payment provider by returning a `RedirectResponse`. + +Also, you need a `finalize` method to properly handle your customer when he was returned to your shop. +This is where you check the payment state and set the order transaction state accordingly. Let's have a look at an example implementation of your custom asynchronous payment handler: -```php -// /src/Service/ExamplePayment.php +::: code-group + +```php [MyCustomPaymentHandler.php] transactionStateHandler = $transactionStateHandler; + public function supports(PaymentHandlerType $type, string $paymentMethodId, Context $context): bool + { + // This payment handler does not support recurring payments nor refunds + return false; } /** - * @throws AsyncPaymentProcessException + * This method is always called during the checkout. + * You should process the payment here and return a RedirectResponse. + * After redirect the finalize method will be called. */ - public function pay(AsyncPaymentTransactionStruct $transaction, RequestDataBag $dataBag, SalesChannelContext $salesChannelContext): RedirectResponse + public function pay(Request $request, PaymentTransactionStruct $transaction, Context $context, ?Struct $validateStruct): ?RedirectResponse { // Method that sends the return URL to the external gateway and gets a redirect URL back try { $redirectUrl = $this->sendReturnUrlToExternalGateway($transaction->getReturnUrl()); } catch (\Exception $e) { - throw PaymentException::asyncProcess( - $transaction->getOrderTransaction()->getId(), + throw PaymentException::asyncProcessInterrupted( + $transaction->getOrderTransactionId(), 'An error occurred during the communication with external payment gateway' . PHP_EOL . $e->getMessage() ); } @@ -172,31 +204,20 @@ class ExamplePayment implements AsynchronousPaymentHandlerInterface } /** - * @throws CustomerCanceledAsyncPaymentException + * This method will be called after redirect from the external payment provider. */ - public function finalize(AsyncPaymentTransactionStruct $transaction, Request $request, SalesChannelContext $salesChannelContext): void + public function finalize(Request $request, PaymentTransactionStruct $transaction, Context $context): void { - $transactionId = $transaction->getOrderTransaction()->getId(); - // Example check if the user canceled. Might differ for each payment provider if ($request->query->getBoolean('cancel')) { - throw PaymentException::asyncCustomerCanceled( - $transactionId, + throw PaymentException::customerCanceled( + $transaction->getOrderTransactionId(), 'Customer canceled the payment on the PayPal page' ); } - // Example check for the actual status of the payment. Might differ for each payment provider - $paymentState = $request->query->getAlpha('status'); - - $context = $salesChannelContext->getContext(); - if ($paymentState === 'completed') { - // Payment completed, set transaction status to "paid" - $this->transactionStateHandler->paid($transaction->getOrderTransaction()->getId(), $context); - } else { - // Payment not completed, set transaction status to "open" - $this->transactionStateHandler->reopen($transaction->getOrderTransaction()->getId(), $context); - } + // handle the payment state + $this->transactionStateHandler->paid($transaction->getOrderTransactionId(), $context); } private function sendReturnUrlToExternalGateway(string $getReturnUrl): string @@ -210,138 +231,233 @@ class ExamplePayment implements AsynchronousPaymentHandlerInterface } ``` -Let's start with the `pay` method. You'll have to start by letting your external payment provider know where he should redirect your customer in return when the payment is done. This is usually done by making an API call and transmitting the return URL, which you can fetch from the passed `AsyncPaymentTransactionStruct` using the method `getReturnUrl`. Since this is just an example, the method `sendReturnUrlToExternalGateway` is empty. Fill in your logic in there in order to actually send the return URL to the external payment provider. The last thing you need to do, is to redirect your customer to the external payment provider via a `RedirectResponse`. +```xml [services.xml] + + + + + + + + + + + +``` -Once your customer is done at the external payment provider, he will be redirected back to your shop. This is where the `finalize` method will be executed. In here, you have to check whether or not the payment process was successful. If e.g., the customer canceled the payment process, you'll have to throw a `CustomerCanceledAsyncPaymentException` exception. +::: -Otherwise, you can proceed to check if the payment status was successful. If so, set the order's transaction state to `paid`. If not, you could, e.g. reopen the order's transaction. +Let's start with the `pay` method. You'll have to start by letting your external payment provider know where he should redirect your customer in return when the payment is done. +This is usually done by making an API call and transmitting the return URL, which you can fetch from the passed `PaymentTransactionStruct` using the method `getReturnUrl`. +Since this is just an example, the method `sendReturnUrlToExternalGateway` is empty. +Fill in your logic in there in order to actually send the return URL to the external payment provider. +The last thing you need to do, is to redirect your customer to the external payment provider by returning a `RedirectResponse`. +Shopware handles the redirect for you automatically. -### Prepared payments example +Once your customer is done at the external payment provider, he will be redirected back to your shop. +This is where the `finalize` method will be executed. +In here, you have to check whether the payment process was successful. +If e.g., the customer canceled the payment process, you'll have to throw a `PaymentException::customerCanceled` exception. -To improve the payment workflow on headless systems or reduce orders without payment, payment handlers can implement an additional interface to support pre-created payments. The client (e.g. a single-page application) can prepare the payment directly with the payment service (not through Shopware) and pass a transaction reference (token) to Shopware to complete the payment. +Otherwise, you can proceed to check if the payment status was successful. +If so, set the order's transaction state to `paid`. +If not, you could, e.g. reopen the order's transaction. -Two steps are necessary: The handler has to validate the payment beforehand, or throw an exception, if the validation fails. After completing the checkout, Shopware calls the handler again to charge the payment. + + + + +To improve the payment workflow on headless systems or reduce orders without payment, payment handlers can implement an additional method to support pre-created payments. +The client (e.g. a single-page application) can prepare the payment directly with the payment service (not through Shopware) and pass a transaction reference (token) to Shopware to complete the payment. + +Two steps are necessary: +The handler has to validate the payment beforehand, or throw an exception, if the validation fails. +If the validation is successful, the payment handler has to capture the payment in the `pay` method. Let's have a look at a simple example: -```php -// /src/ExamplePayment.php +::: code-group + +```php [MyCustomPaymentHandler.php] stateHandler = $stateHandler; + // This payment handler does not support recurring payments nor refunds + return false; } - public function validate( - Cart $cart, - RequestDataBag $requestDataBag, - SalesChannelContext $context - ): Struct { - if (!$requestDataBag->has('my-payment-token')) { + /** + * This method will always be called during the checkout, but before order creation. + * This is especially helpful for headless systems and single-page applications, that prepare payments not through Shopware. + */ + public function validate(Cart $cart, RequestDataBag $dataBag, SalesChannelContext $context): ?Struct + { + // the payment is prepared here and most certainly you will receive a token from your PSP + if (!$dataBag->has('my-payment-token')) { // this will fail the validation - throw PaymentException::preparedValidate('No token supplied'); + throw PaymentException::validatePreparedPaymentInterrupted('No token supplied'); } - $token = $requestDataBag->get('my-payment-token'); - $paymentData = $this->getPaymentDataFromProvider($token); - - if (!$paymentData) { - // no payment data simulates an error response from our payment provider in this example - throw PaymentException::preparedValidate('Unknown payment for token ' . $token); - } + $token = $dataBag->get('my-payment-token'); // other checks could include comparing the cart value with the actual payload of your PSP // return the payment details: these will be given as $preOrderPaymentStruct to the capture method - return new ArrayStruct($paymentData); + return new ArrayStruct(['my-payment-provider-transaction-token' => $token]); } - public function capture( - PreparedPaymentTransactionStruct $transaction, - RequestDataBag $requestDataBag, - SalesChannelContext $context, - Struct $preOrderPaymentStruct - ): void { + /** + * This method is always called during the checkout, but only after the validate method was called. + * You should process the payment here and return a RedirectResponse if the payment process requires an asynchronous approach. + * In that case, the finalize method will be called additionally during checkout after the redirect. + * If the payment process is synchronous, you should return null. + */ + public function pay(Request $request, PaymentTransactionStruct $transaction, Context $context, ?Struct $validateStruct): ?RedirectResponse + { + $validateData = $validateStruct->getVars(); - // you can find all the order specific information in the PreparedPaymentTransactionStruct - $order = $transaction->getOrder(); - $orderTransaction = $transaction->getOrderTransaction(); + if (!isset($validateData['my-payment-provider-transaction-token'])) { + // this will fail the payment process + throw PaymentException::syncProcessInterrupted($transaction->getOrderTransactionId(), 'No payment token provided'); + } + + // In here you should probably call your payment provider to precess the payment and compare tokens + if ($validateData['my-payment-provider-transaction-token'] !== 'hatoooken') { + // this will fail the payment process + throw PaymentException::syncProcessInterrupted($transaction->getOrderTransactionId(), 'Payment token mismatch'); + } - // call you PSP and capture the transaction as usual - // do not forget to change the transaction's state here: - $this->stateHandler->paid($orderTransaction->getId(), $context->getContext()); + // In here you should probably call your payment provider to precess the payment + // $this->myPaymentProvider->processPayment($transaction); - // or in case of an error: - $this->stateHandler->fail($orderTransaction->getId(), $context->getContext()); - throw PaymentException::preparedCapture($orderTransaction->getId(), 'Capture failed.'); - } + // afterward you should update the transaction with the new state + $this->transactionStateHandler->process($transaction->getOrderTransactionId(), $context); - private function getPaymentDataFromProvider(string $token): array - { - // call your payment provider instead and return your real payment details - return []; + return null; } } ``` -### Refund example +```xml [services.xml] + + + + + + + + + + + +``` + +::: + + + + -To allow easy refund handling, Shopware introduced a centralized way of handling refund for transactions. +To allow easy refund handling, your payment handler should return `true` in the supports method, +whenever the PaymentHandlerType is REFUND. -For this, have your payment handler implement the `RefundPaymentHandlerInterface`. +Let's look at a short example of how to implement a refund handlers. -Let's look at a short example of how to implement such payment handlers. +::: code-group -```php -// /src/ExamplePayment.php +```php [MyCustomPaymentHandler.php] stateHandler = $stateHandler; + // this payment handler supports refunds + return $type === PaymentHandlerType::REFUND; } - public function refund(OrderTransactionCaptureRefundEntity $refund, Context $context): void + /** + * This method is always called during the checkout. + * You should process the payment here and return a RedirectResponse if the payment process requires an asynchronous approach. + * In that case, the finalize method will be called additionally during checkout after the redirect. + * If the payment process is synchronous, you should return null. + */ + public function pay(Request $request, PaymentTransactionStruct $transaction, Context $context, ?Struct $validateStruct): ?RedirectResponse { - if ($refund->getAmount() > 100.00) { + // In here you should probably call your payment provider to precess the payment + // $this->myPaymentProvider->processPayment($transaction); + + // afterward you should update the transaction with the new state + $this->transactionStateHandler->process($transaction->getOrderTransactionId(), $context); + + return null; + } + + /** + * As long as the supports method returns true for PaymentHandlerType::REFUND, this method will be called during the refund process. + */ + public function refund(RefundPaymentTransactionStruct $transaction, Context $context): void + { + $refund = $this->getRefund($transaction->getRefundId(), $context); + + if (!$refund) { // this will stop the refund process and set the refunds state to `failed` - throw PaymentException::refund($refund->getId(), 'Refunds over 100 € are not allowed'); + throw PaymentException::refundInterrupted($transaction->getRefundId(), 'Refund not found'); } // a refund can have multiple positions, with different order line items and amounts - /** @var OrderTransactionCaptureRefundPositionEntity $position */ foreach ($refund->getPositions() as $position) { $amount = $position->getAmount()->getTotalPrice(); $reason = $position->getReason(); @@ -350,88 +466,179 @@ class ExamplePayment implements RefundPaymentHandlerInterface // let's say, you allow a position, which was delivered, however broken if ($reason === 'malfunction') { // you can call your PSP here to refund - try { $this->callPSPForRefund($amount, $reason, $lineItem->getId()); } catch (\Exception $e) { // something went wrong at PSP side, throw a refund exception // this will set the refund state to `failed` - throw PaymentException::refund($refund->getId(), 'Something went wrong'); + throw PaymentException::refundInterrupted($refund->getId(), 'Something went wrong'); } } } // let Shopware know, that the refund was successful - $this->stateHandler->complete($refund->getId(), $context); + $this->refundStateHandler->complete($refund->getId(), $context); } - private function callPSPForRefund(float $amount, string $reason, string $id): void + private function getRefund(string $refundId, Context $context): ?OrderTransactionCaptureRefundEntity { - // call you PSP here and process the response - // throw an exception to stop the refund process + return $this->refundRepository->search(new Criteria([$refundId]), $context)->first(); + } + + private function callPSPForRefund(float $amount, string $reason, string $lineItemId): void + { + // call your PSP here } } + +``` + +```xml [services.xml] + + + + + + + + + + + + + ``` +::: + As you can see, you have complete control over handling the refund request and which positions to refund. -### Recurring capture example + + + + +Recurring payment handlers allow continuous charging of a customer's payment method. +This is especially useful for subscription-based services. + +::: info +A full-fledged Subscriptions feature with recurring payments is available exclusively through our [paid plans](https://www.shopware.com/en/pricing/). +::: ::: info -Recurring orders and payments require the Subscriptions feature, available exclusively in our [paid plans](https://www.shopware.com/en/pricing/). +Usually, a billing agreement between the customer and the payment provider is necessary to allow recurring payments. ::: -```php -// /src/ExamplePayment.php +::: code-group + +```php [MyCustomPaymentHandler.php] transactionStateHandler = $transactionStateHandler; + // this payment handler supports recurring payments + return $type === PaymentHandlerType::RECURRING; } /** - * @throws RecurringPaymentProcessException + * This method is in the case of recurring payments only called during the initial checkout. + * You probably want to create a billing agreement between the customer and your PSP here. + * Do not forget to capture the initial charge as well. */ - public function captureRecurring(RecurringPaymentTransactionStruct $transaction, Context $context): void + public function pay(Request $request, PaymentTransactionStruct $transaction, Context $context, ?Struct $validateStruct): ?RedirectResponse { - // call your PSP here for capturing a recurring payment - // a valid billing agreement between the customer and the PSP should already be in place - // use on of the other payment interfaces to create such an agreement on checkout and capture the initial order once - // you will probably receive a token from your PSP for the billing agreement, which you will need to capture a recurring payment + // You can identify, that this is the intitial capture for a recurring payment by checking the `RecurringDataStruct` in the `PaymentTransactionStruct` + if ($transaction->getRecurring()) { + // In here you should probably call your payment provider to create a billing agreement + // $this->myPaymentProvider->createBillingAgreement($transaction); + } + // Don't forget to capture the initial payment as well + // $this->myPaymentProvider->processPayment($transaction); + + // afterward you should update the transaction with the new state + $this->transactionStateHandler->process($transaction->getOrderTransactionId(), $context); + + return null; + } + + /** + * call your PSP here for capturing a recurring payment + * a valid billing agreement between the customer and the PSP should usually already be in place + * use on of the other payment handler methods to create such an agreement on checkout and capture the initial order once + * you will probably receive a token from your PSP for the billing agreement, which you will need to capture a recurring payment + */ + public function recurring(PaymentTransactionStruct $transaction, Context $context): void + { try { - // $this->callMyPsp(); + $data = $transaction->getRecurring()?->getVars(); + + if (!$data || !isset($data['pspBillingAgreementToken'])) { + throw PaymentException::recurringInterrupted($transaction->getOrderTransactionId(), 'No token supplied'); + } + + //$this->callPSPForRecurringPayment($data['pspBillingAgreementToken']); } catch (\Throwable $e) { - // throw a RecurringPaymentProcessException: this will set the transaction state to `failed` - throw PaymentException::recurringInterrupted($transaction->getOrderTransaction()->getId(), 'Something went wrong', $e); + // throw a PaymentException::recurringInterrupted to set the transaction state to `failed` + throw PaymentException::recurringInterrupted($transaction->getOrderTransactionId(), 'Something went wrong', $e); } } } + ``` -## Setting up new payment method +```xml [services.xml] + -The handler itself is not used yet, since there is no payment method actually using the handler created above. In short: Your handler is not handling any payment method so far. The payment method can be added to the system while installing your plugin. + + + + + + + + + +``` + +::: + + + + + +## Setting up the new payment method + +The handler itself is not used yet, since there is no payment method actually using the handler created above. +In short: Your handler is not handling any payment method so far. +The payment method can be added to the system while installing your plugin. An example for your plugin could look like this: -```php -// /src/SwagBasicExample.php +::: code-group + +```php [SwagBasicExample.php] container->get(PluginIdProvider::class); $pluginId = $pluginIdProvider->getPluginIdByBaseClass(get_class($this), $context); $examplePaymentData = [ // payment handler will be selected by the identifier - 'handlerIdentifier' => ExamplePayment::class, + 'handlerIdentifier' => MyCustomPaymentHandler::class, 'name' => 'Example payment', 'description' => 'Example payment description', 'pluginId' => $pluginId, // if true, payment method will also be available after the order // is created, e.g. if payment fails and the user wants to try again 'afterOrderEnabled' => true, + // the technicalName helps you to identify the payment method uniquely + // it is best practice to use a plugin specific prefix to avoid conflicts + 'technicalName' => 'swag_example-example_payment', ]; - /** @var EntityRepository $paymentRepository */ $paymentRepository = $this->container->get('payment_method.repository'); $paymentRepository->create([$examplePaymentData], $context); } private function setPaymentMethodIsActive(bool $active, Context $context): void { - /** @var EntityRepository $paymentRepository */ $paymentRepository = $this->container->get('payment_method.repository'); $paymentMethodId = $this->getPaymentMethodId(); @@ -525,7 +732,6 @@ class SwagBasicExample extends Plugin private function getPaymentMethodId(): ?string { - /** @var EntityRepository $paymentRepository */ $paymentRepository = $this->container->get('payment_method.repository'); // Fetch ID for update @@ -535,14 +741,80 @@ class SwagBasicExample extends Plugin } ``` -In the `install` method, you start by creating a new payment method, if it doesn't exist yet. If you need to know what's happening in there, you might want to have a look at our guide regarding [Writing data](../../framework/data-handling/writing-data). +::: -::: warning -**Do not** do the opposite in the `uninstall` method and remove the payment method. This might lead to data inconsistency if the payment method was used in some orders. Instead, only deactivate the method! +In the `install` method, you start by creating a new payment method, if it doesn't exist yet. +If you need to know what's happening in there, you might want to have a look at our guide regarding [Writing data](../../framework/data-handling/writing-data). + +::: danger +**Do not** do the opposite in the `uninstall` method and remove the payment method. +This might lead to data inconsistency if the payment method was used in some orders. +Instead, only deactivate the method! ::: The `activate` method and `deactivate` method just do that, activating and deactivating the payment method, respectively. ### Identify your payment -You can identify your payment by the entity property `formattedHandlerIdentifier`. It shortens the original handler identifier \(php class reference\): `Custom/Payment/SEPAPayment` to `handler_custom_sepapayment`. The syntax for the shortening can be looked up in [Shopware\Core\Checkout\Payment\DataAbstractionLayer\PaymentHandlerIdentifierSubscriber](https://github.com/shopware/shopware/blob/v6.3.4.1/src/Core/Checkout/Payment/DataAbstractionLayer/PaymentHandlerIdentifierSubscriber.php). +You can identify your payment by the entity property `formattedHandlerIdentifier`. +It shortens the original handler identifier \(php class reference\): `Custom/Payment/SEPAPayment` to `handler_custom_sepapayment`. +The syntax for the shortening can be looked up in [Shopware\Core\Checkout\Payment\DataAbstractionLayer\PaymentHandlerIdentifierSubscriber](https://github.com/shopware/shopware/blob/v6.3.4.1/src/Core/Checkout/Payment/DataAbstractionLayer/PaymentHandlerIdentifierSubscriber.php). + +Otherwise, you can use your given technical name to uniquely identify your payment method. + +## Migrating payment handlers from 6.6 + +If you are migrating a payment handler from a version before 6.7, +you need to move from the existing interfaces to the new abstract class and add your own order data loading.. + +### Payment handler interfaces removed + +Instead of implementing multiple interfaces of `Shopware\Core\Checkout\Payment\Cart\PaymentHandler\PaymentHandlerInterface` in your payment handler, +extend the `Shopware\Core\Checkout\Payment\Cart\PaymentHandler\AbstractPaymentHandler` class and implement the necessary methods. + +| Old interface | Method used in payment handler | Checks for `supports` method | +|---------------------------------------|-----------------------------------------------------------------------------------------------------------|---------------------------------| +| `SynchronousPaymentHandlerInterface` | `pay`: always called during checkout | - | +| `AsynchronousPaymentHandlerInterface` | `finalize`: only called, if `pay` returns a `RedirectResponse` | - | +| `PreparedPaymentHandlerInterface` | `validate`: be aware that this method is always called and can be used to validate a cart during checkout | - | +| `RecurringPaymentHandlerInterface` | `recurring` | `PaymentHandlerType::RECURRING` | +| `RefundPaymentHandlerInterface` | `refund` | `PaymentHandlerType::REFUND` | + +### New single tag for payment handlers + +Your payment handler should now have a single tag in the service definition: `shopware.payment.method`. +Remove any other occurrences of the following tags: +`shopware.payment.method.sync`, `shopware.payment.method.async`, `shopware.payment.method.prepared`, `shopware.payment.method.recurring`, `shopware.payment.method.refund`. + +::: code-group + +```xml [services.xml] + + + + + + + + + + + + + + + + + + +``` +::: + +### Prepared payments + +In the past, you would have to implement the `validate` and `capture` methods when dealing with the `PreparedPaymentHandlerInterface`. + +Now, you only have to implement the `validate` method. +Instead of the `capture` method, the streamlined `pay` method is used and has to be implemented. diff --git a/guides/plugins/plugins/checkout/payment/customize-payment-provider.md b/guides/plugins/plugins/checkout/payment/customize-payment-provider.md index 62d8e1762..c574d3503 100644 --- a/guides/plugins/plugins/checkout/payment/customize-payment-provider.md +++ b/guides/plugins/plugins/checkout/payment/customize-payment-provider.md @@ -9,18 +9,26 @@ nav: ## Overview -In this guide you'll learn how to customize an existing payment provider. In this example we are customizing a `SynchronousPaymentHandler`, but the procedure also applies to `AsynchronousPaymentHandler`. +In this guide you'll learn how to customize an existing payment provider. +In this example we are customizing a synchronous payment flow, but the procedure also applies to an asynchronous approach. ## Prerequisites -As most guides, this guide is also built upon the [Plugin base guide](../../plugin-base-guide), but you don't necessarily need that. It is helpful to have looked at the guide about [adding a custom payment method](add-payment-plugin) beforehand. Furthermore, decorating a service is also not explained here, but it's covered in our guide about [adjusting a service](../../plugin-fundamentals/adjusting-service), so having this open in another tab won't hurt. +As most guides, this guide is also built upon the [Plugin base guide](../../plugin-base-guide), but you don't necessarily need that. +It is helpful to have looked at the guide about [adding a custom payment method](add-payment-plugin) beforehand. +Furthermore, decorating a service is also not explained here, but it's covered in our guide about [adjusting a service](../../plugin-fundamentals/adjusting-service), so having this open in another tab won't hurt. ## Customize the payment provider -First, we create a new class that extends from the provider we want to customise. In this example we customise the class `Shopware\Core\Checkout\Payment\Cart\PaymentHandler\DebitPayment` and name our class `ExampleDebitPayment`. The constructor has to accept an instance of `OrderTransactionStateHandler` like the original service and additionally an instance of `DebitPayment` that we want to decorate. +First, we create a new class that extends from the provider we want to customise. +In this example we customise the class `Shopware\Core\Checkout\Payment\Cart\PaymentHandler\DebitPayment` and name our class `ExampleDebitPayment`. +The constructor has to accept an instance of `OrderTransactionStateHandler` like the original service and additionally an instance of `DebitPayment` that we want to decorate. -```php -// /src/Service/ExampleDebitPayment.php +After we've created our customized payment provider class, we have to register it to the DI-container via the `services.xml`. + +::: code-group + +```php [ExampleDebitPayment.php] decorated; } - public function pay(SyncPaymentTransactionStruct $transaction, RequestDataBag $dataBag, SalesChannelContext $salesChannelContext): void + public function pay(Request $request, PaymentTransactionStruct $transaction, Context $context, ?Struct $validateStruct): ?RedirectResponse { // do some custom stuff here @@ -55,10 +66,7 @@ class ExampleDebitPayment extends DebitPayment } ``` -After we've created our customized payment provider class, we have to register it to the DI-container. - -```xml -// /src/Resources/config/services.xml +```xml [services.xml] - + synchronousPayment Synchronous payment Synchrone Zahlung This payment method does everything in one request. Diese Zahlungsmethode arbeitet in einem Request. - https://payment.app/sync/process + https://payment.app/sync/pay - + simpleSynchronousPayment Simple Synchronous payment Einfache synchrone Zahlung @@ -42,18 +42,18 @@ - + preparedPayment Payment, that offers everything Eine Zahlungsart, die alles kann - https://payment.app/prepared/validate - https://payment.app/prepared/capture + https://payment.app/validate + https://payment.app/pay Resources/paymentLogo.png - + refundPayment Refund payments Einfache Erstattungen @@ -63,7 +63,7 @@ - + recurringPayment Recurring payments Einfache wiederkehrende Zahlungen