Introducing payment plugins

On this page, you will find an overview of relevant information about payment plugins. In the first chapter, we will explain the workflow and interaction of payment plugins with the plentymarkets Ceres and IO plugins. In the second chapter, you will find a short description for each plugin feature that you can use to create your own payment plugins.

Further reading

Payment plugin workflow

The flowchart below describes the general workflow of payment plugins and the interaction of payment and template plugins.

Getting the payment method content

Active payment plugins will be displayed in the checkout of the template plugin. When the customer clicks on the Order now button, the GetPaymentMethodContent event is triggered. Depending on the content type the following results are possible:

Type Description
errorCode The payment will not be prepared. An error message will be displayed on the Checkout page.
continue The payment will be processed by the IO plugin. Payment plugins that do not require specific code for displaying own content in the template or redirecting to a payment provider can use this type.
externalContentUrl;
htmlContent
Payment plugins with specific code for displaying own content in the template can use these types to show either HTML content or external content by defining an external content URL. A pop-up window will be displayed on the Checkout page. The customer must click Confirm to continue the payment process.
redirectUrl The customer will be forwarded to the payment provider. After entering the required data on the payment provider page, the customer will be directed back and the payment plugin continues the payment process with the entered payment data.

Creating the order

The order will be created. This can be done in two different ways:

  • IO: An order will be created by the IO plugin using the place-order URL. Then, the executePayment event is triggered in the IO plugin. If no order is created, an error message will be displayed on the Checkout page.
  • Payment plugin: An order will be created by the payment plugin. Then, the execute-payment URL is used to trigger the executePayment event in the payment plugin. If no order is created, an error message will be displayed on the Checkout page.

Executing the payment

The executePayment event is triggered. The payment plugin checks whether the payment is executed. If the payment is executed, the customer will be forwarded to the Confirmation page displaying an overview of the order. If no payment is executed, the customer will also be forwarded to the Confirmation page, but an Order not paid note will be displayed.

Payment plugin features

Find detailed information about payment plugin features below. The PayPal plugin developed by plentymarkets is used as a reference for explaining payment plugin features. The PayPal plugin can be the starting point for developing other payment plugins.

Registering payment methods

In order to make a payment method available for a plentymarkets system, the payment method must be registered by the plugin. This is done in the ServiceProvider that is saved in the src/providers folder. The ServiceProvider itself is specified in the plugin.json file. In the ServiceProvider a payment method is registered with the boot() method. Multiple payment methods can be registered that way. A payment method is registered with a unique key consisting of the PluginKey and the PaymentKey. Registering a payment method is always based on one or multiple events. When the event is triggered, the payment method is loaded.

PayPal/src/providers/PayPalServiceProvider.php
...

/**
 * Boot additional PayPal services
 *
 * @param Dispatcher               $eventDispatcher
 * @param PaymentHelper            $paymentHelper
 * @param PaymentService           $paymentService
 * @param BasketRepositoryContract $basket
 * @param PaymentMethodContainer   $payContainer
 * @param EventProceduresService   $eventProceduresService
 */
  public function boot( Dispatcher $eventDispatcher, PaymentHelper $paymentHelper, PaymentService $paymentService,
                          BasketRepositoryContract $basket, PaymentMethodContainer $payContainer, EventProceduresService $eventProceduresService)
  {
        // Register the PayPal Express payment method in the payment method container
        $payContainer->register('plentyPayPal::PAYPALEXPRESS', PayPalExpressPaymentMethod::class,
                                [ AfterBasketChanged::class, AfterBasketItemAdd::class, AfterBasketCreate::class ]);
        // Register the PayPal payment method in the payment method container
        $payContainer->register('plentyPayPal::PAYPAL', PayPalPaymentMethod::class,
                                [ AfterBasketChanged::class, AfterBasketItemAdd::class, AfterBasketCreate::class ]);

...

Defining payment methods

Every payment method that was registered as described above can provide different values for the template. These values can either be generated by functions or read from the configuration of the plugin. This information is saved in the PayPalPaymentMethod.php file in the src/methods folder. The following functions are currently available:

  • isActive():bool
  • isSelectable():bool
  • isExpressCheckout():bool
  • getName():string
  • getFee():float
  • getIcon():string
  • getDescription():string
  • getSourceUrl():string
PayPal/src/methods/PayPalPaymentMethod.php
<?php

namespace PayPal\Methods;

use Plenty\Modules\Account\Contact\Contracts\ContactRepositoryContract;
use Plenty\Modules\Basket\Contracts\BasketRepositoryContract;
use Plenty\Modules\Payment\Method\Contracts\PaymentMethodService;
use Plenty\Plugin\ConfigRepository;

/**
 * Class PayPalPaymentMethod
 * @package PayPal\Methods
 */
class PayPalPaymentMethod extends PaymentMethodService
{

    ...

    /**
     * Check whether the plugin is active. Active plugins are displayed in the list of payment methods.
     *
     * @return bool
     */
    public function isActive()
    {
        return true;
    }
    /**
     * Check whether the plugin is selectable. If the plugin is active and not selectable, it is displayed, but cannot be chosen as payment method. Default is true.
     *
     * @return bool
     */
    public function isSelectable()
    {
        return true;
    }
    /**
     * Check whether the plugin can be used as Express Checkout. Default is false.
     *
     * @return bool
     */
    public function isExpressCheckout()
    {
        return false;
    }

    /**
     * Get the name of the plugin
     *
     * @return string
     */
    public function getName()
    {
        $name = $this->configRepo->get('PayPal.name');
        if(!strlen($name))
        {
            $name = 'PayPal';
        }
        return $name;
    }

    /**
     * Get additional costs for PayPal. Additional costs can be entered in the config.json.
     *
     * @return float
     */
    public function getFee()
    {
        $fee = $this->configRepo->get('PayPal.fee');
        if(strlen($fee))
        {
            $fee = str_replace(',', '.', $fee);
        }
        else
        {
            $fee = 0;
        }
        return (float)$fee;
    }

    /**
     * Get the path of the icon
     *
     * @return string
     */
    public function getIcon()
    {
        $icon = 'layout/plugins/production/paypal/images/logos/de-pp-logo.png';
        return $icon;
    }

    /**
     * Get the description of the payment method. The description can be entered in the back end UI of the plugin and is limited to 150 characters.
     *
     * @return string
     */
    public function getDescription()
    {
        /** @var FrontendSessionStorageFactoryContract $session */
        $session = pluginApp(FrontendSessionStorageFactoryContract::class);
        $lang = $session->getLocaleSettings()->language;

        if( array_key_exists('language', $this->paymentService->settings) &&
            array_key_exists($lang, $this->paymentService->settings['language']) &&
            array_key_exists('description', $this->paymentService->settings['language'][$lang]))
        {
            return $this->paymentService->settings['language'][$lang]['description'];
        }
        return '';
    }

    /**
     * Get the details page for the payment method. This can be a category page or an external page, e.g., the homepage of the payment provider.
     *
     * @return string
     */
    public function getSourceUrl()
    {
        /** @var FrontendSessionStorageFactoryContract $session */
        $session = pluginApp(FrontendSessionStorageFactoryContract::class);
        $lang = $session->getLocaleSettings()->language;

        if( array_key_exists('infoPageType', $this->paymentService->settings))
        {
            if( array_key_exists('language', $this->paymentService->settings) &&
                array_key_exists($lang, $this->paymentService->settings['language']))
            {
                switch ($this->paymentService->settings['infoPageType'])
                {
                    case 1:
                        if(array_key_exists('internalInfoPage', $this->paymentService->settings['language'][$lang]))
                        {
                            // internal
                            $categoryId = (int) $this->paymentService->settings['language'][$lang]['internalInfoPage'];
                            if($categoryId  > 0)
                            {
                                /** @var CategoryRepositoryContract $categoryContract */
                                $categoryContract = pluginApp(CategoryRepositoryContract::class);
                                return $categoryContract->getUrl($categoryId, $lang);
                            }
                        }
                        return '';
                    case 2:
                        if(array_key_exists('externalInfoPage', $this->paymentService->settings['language'][$lang]))
                        {
                            return $this->paymentService->settings['language'][$lang]['externalInfoPage'];
                        }
                        return '';
                    default:
                        return '';
                }
            }
        }
        return '';
    }
}

Registering event listener and events

In order to respond to different events, a listener for the respective events must be registered. The listener is registered in the boot() method of the ServiceProvider. Every event to be responded to must be registered here, too.

PayPal/src/providers/PayPalServiceProvider.php
...

// Listen for the event that gets the payment method content
$eventDispatcher->listen(GetPaymentMethodContent::class,
                   function(GetPaymentMethodContent $event) use( $paymentHelper, $basket, $paymentService)
                   {
                        if($event->getMop() == $paymentHelper->getPayPalMopId())
                        {
                              $basket = $basket->load();
                              $event->setValue($paymentService->getPaymentContent($basket));
                              $event->setType( $paymentService->getReturnType());
                        }
                   });

// Listen for the event that executes the payment
$eventDispatcher->listen(ExecutePayment::class,
                  function(ExecutePayment $event) use ( $paymentHelper, $paymentService)
                  {
                        if($event->getMop() == $paymentHelper->getPayPalMopId())
                        {
                              // Execute the payment
                              $payPalPayment = $paymentService->executePayment();
                              // Check whether the PayPal payment has been executed successfully
                              if($paymentService->getReturnType() != 'errorCode')
                              {
                                    // Create a payment in plentymarkets with the PayPal payment data
                                    $plentyPayment = $paymentHelper->createPlentyPaymentFromJson($payPalPayment);
                                    if($plentyPayment instanceof Payment)
                                    {
                                          // Assign the payment to an order in plentymarkets
                                          $paymentHelper->assignPlentyPaymentToPlentyOrder($plentyPayment, $event->getOrderId());
                                          $event->setType('success');
                                          $event->setValue('The payment has been executed successfully!');
                                    }
                              }
                              else
                              {
                                  $event->setType('error');
                                  $event->setValue('The PayPal payment could not be executed!');
                              }
                        }
                  });

...

Registering routes

A plugin can register its own routes that can be used to map specific functions. These routes are used, e.g. as end points for payment confirmations or other notifications.

PayPal/src/providers/PayPalRouteServiceProvider.php
<?php

namespace PayPal\Providers;

use Plenty\Plugin\RouteServiceProvider;
use Plenty\Plugin\Routing\Router;

/**
 * Class PayPalRouteServiceProvider
 * @package PayPal\Providers
 */
class PayPalRouteServiceProvider extends RouteServiceProvider
{
    /**
     * @param Router $router
     */
    public function map(Router $router)
    {
        // Get the PayPal success and cancellation URLs
        $router->get('payPal/checkoutSuccess', 'PayPal\Controllers\PaymentController@checkoutSuccess');
        $router->get('payPal/checkoutCancel' , 'PayPal\Controllers\PaymentController@checkoutCancel' );
        $router->get('payPal/expressCheckout', 'PayPal\Controllers\PaymentController@expressCheckout');
        $router->post('payPal/notification'  , 'PayPal\Controllers\PaymentNotificationController@handleNotification');
    }
}

Using template functions

In order to load particular content into the layout of the online store, the paymentContent is used and filled by the plugin. This is done with the help of an event. The plugin must respond to the event and the event will provide the respective content. The GetPaymentMethodContent event registered in the ServiceProvider must set the content and the content type. The following paymentContent types are available:

  • htmlContent
  • externalContentUrl
  • redirectUrl
  • errorCode
  • continue
PayPal/src/services/PaymentService.php
...

/**
 * Get the PayPal payment content
 *
 * @param Basket $basket
 * @return string
 */
public function getPaymentContent(Basket $basket, $mode = 'paypal'):string
{

...

    // Get the content of the PayPal container
    $paymentContent = '';
    $links = $resultJson->links;
    if(is_array($links))
    {
        foreach($links as $key => $value)
        {
            // Get the redirect URLs for the content
            if($value->method == 'REDIRECT')
            {
                $paymentContent = $value->href;
                $this->returnType = 'redirectUrl';
            }
        }
    }
    // Check whether the content is set. Else, return an error code.
    if(!strlen($paymentContent))
    {
        $this->returnType = 'errorCode';
        return 'An unknown error occurred, please try again.';
    }
    return $paymentContent;
}

...

Creating payments

An order should only be further processed in the plentymarkets system, if a payment is assigned to the order. Therefore the plugin must ensure that a payment is created and assigned. Depending on the payment method, assigning a payment can be done right after placing an order, e.g. by responding to the respective event. Another possibility to create a payment is by calling a certain route. The payment must be structured according to the Payment model.

PayPal/src/helper/PaymentHelper.php
...

/**
 * Create a payment in plentymarkets from the JSON data
 *
 * @param string $json
 * @return Payment
 */
public function createPlentyPaymentFromJson(string $json)
{
    $payPalPayment = json_decode($json);
    $paymentData = array();
    // Set the payment data
    $paymentData['mopId']           = (int)$this->getPayPalMopId();
    $paymentData['transactionType'] = 2;
    $paymentData['status']          = $this->mapStatus($payPalPayment->status);
    $paymentData['currency']        = $payPalPayment->currency;
    $paymentData['amount']          = $payPalPayment->amount;
    $paymentData['receivedAt']       = $payPalPayment->entryDate;
    $payment = $this->paymentRepository->createPayment($paymentData);
    /**
     * Add payment property with type booking text
     */
    $this->addPaymentProperty($payment->id, array('typeId'=>3, 'value'=>'PayPalPayID: '.(string)$payPalPayment->bookingText));
    /**
     * Add payment property with type origin
     */
    $originConstants        = $this->paymentRepository->getOriginConstants();
    $paymentPropertyValue      = '';
    if(!is_null($originConstants) && is_array($originConstants))
    {
        $paymentPropertyValue = (string)$originConstants['plugin'];
    }
    $this->addPaymentProperty($payment->id, array('typeId'=>23, 'value'=>$paymentPropertyValue));
    return $payment;
}

...

Assigning payments to orders

After creating a payment, the payment can be assigned to an order. For this purpose a relation is created in the assignPlentyPaymentToPlentyOrder method that uses the OrderRepositoryContract.

PayPal/src/helper/PaymentHelper.php
...

/**
 * Assign the payment to an order in plentymarkets
 *
 * @param Payment $payment
 * @param int $orderId
 */
public function assignPlentyPaymentToPlentyOrder(Payment $payment, int $orderId)
{
    // Get the order by the given order ID
    $order = $this->orderRepo->findOrderById($orderId);
    // Check whether the order truly exists in plentymarkets
    if(!is_null($order) && $order instanceof Order)
    {
        // Assign the given payment to the given order
        $this->paymentOrderRelationRepo->createOrderRelation($payment, $order);
    }
}

...

Rejecting payments

When a payment provider rejects a payment, this information must be saved in the payment. This is done with the help of the payment status. The payment status can be changed. The plugin can change the status of a payment via a predefined route. For this purpose, the PaymentRepositoryContract with the updatePayment method must be used.

Retrieving addresses from the payment provider

Provided that the payment method requires the addresses from the payment provider, a new Contact must be created in the online store as soon as the addresses are available. To do so, the ContactRepositoryContract must be used in the ContactService.php file.

PayPal/src/services/ContactService.php
<?php

namespace PayPal\Services;

use Plenty\Modules\Account\Contact\Contracts\ContactRepositoryContract;
use Plenty\Modules\Account\Contact\Models\Contact;

/**
 * Class ContactService
 * @package PayPal\Services
 */
class ContactService
{
    /**
     * @var ContactRepositoryContract
     */
    private $contactRepository;
    /**
     * ContactService constructor.
     * @param ContactRepositoryContract $contactRepository
     */
    public function __construct(ContactRepositoryContract $contactRepository)
    {
        $this->contactRepository = $contactRepository;
    }

    /**
     * Get a contact by ID
     *
     * @param int $contactId
     * @return Contact
     */
    public function getContactById(int $contactId):Contact
    {
        return $this->contactRepository->findContactById($contactId);
    }

    /**
     * Create a contact
     *
     * @param array $contact
     * @return Contact
     */
    public function createContact(array $contact):Contact
    {
        return $this->contactRepository->createContact($contact);
    }
}

A Contact must be structured according to the Contact model. Any number of addresses can be saved in the Contact. Addresses are divided into two types - delivery addresses and invoice addresses. Both types must be structured according to the Address model.

Mapping payment statuses

If you want to use the payment statuses of a payment provider and map them to the plentymarkets payment statuses, you can use the mapStatus method.

PayPal/src/helper/PaymentHelper.php
...

/**
 * Map the PayPal payment status to the plentymarkets payment status
 *
 * @param string $status
 * @return int
 *
 */
public function mapStatus(string $status)
{
    if(!is_array($this->statusMap) || count($this->statusMap) <= 0)
    {
        $statusConstants = $this->paymentRepository->getStatusConstants();
        if(!is_null($statusConstants) && is_array($statusConstants))
        {
            $this->statusMap['created']               = $statusConstants['captured'];
            $this->statusMap['approved']              = $statusConstants['approved'];
            $this->statusMap['failed']                = $statusConstants['refused'];
            $this->statusMap['partially_completed']   = $statusConstants['partially_captured'];
            $this->statusMap['completed']             = $statusConstants['captured'];
            $this->statusMap['in_progress']           = $statusConstants['awaiting_approval'];
            $this->statusMap['pending']               = $statusConstants['awaiting_approval'];
            $this->statusMap['refunded']              = $statusConstants['refunded'];
            $this->statusMap['denied']                = $statusConstants['refused'];
        }
    }
    return (int)$this->statusMap[$status];
}

...

Providing content for template containers

Buttons, logos or other content to be displayed in the template can be made available for template plugins with the help data providers. A data provider is the source for content. A content container in the layout is the target. If a data provider is linked to a content container, the content provided by the data provider is displayed in the content container.

PayPal/src/providers/PayPalExpressButtonDataProvider.php
<?php

namespace PayPal\Providers;

use Plenty\Plugin\Templates\Twig;

/**
 * Class PayPalExpressButtonDataProvider
 * @package PayPal\Providers
 */
class PayPalExpressButtonDataProvider
{
    /**
     * @param Twig $twig
     * @param $args
     * @return string
     */
    public function call( Twig $twig, $args)
    {
        return $twig->render('PayPal::PayPalExpress.PayPalExpressButton');
    }
}

Linking content to containers

In the plentymarkets back end, you can link the content to one or multiple containers. To do so, go to CMS » Container links and activate the content in the desired container. In the image below, the PayPal Express button is linked to the AfterCheckoutButton container.

Displaying the content in the online store

A large number of content containers are available in different views of the template, e.g. the shopping cart preview, the item view, the checkout etc. Let's have a look at how these containers are implemented in the template.

Ceres/resources/views/Basket/Components/BasketPreview.twig
<div class="col-xs-12 col-sm-6">
{{ LayoutContainer.show("Ceres::BasketPreview.BeforeCheckoutButton") }}
    <a v-resource-if:user="isLoggedIn" href="/checkout" class="btn btn-primary btn-block checkOutBtn" title="{{ trans("Ceres::Ceres.basketToCheckout") }}">
    {{ trans("Ceres::Ceres.basketToCheckout") }} <i class="fa fa-arrow-right" aria-hidden="true"></i>
    </a>
    <a v-resource-if:user="!isLoggedIn" href="/login" class="btn btn-primary btn-block checkOutBtn" title="{{ trans("Ceres::Ceres.basketToCheckout") }}">
    {{ trans("Ceres::Ceres.basketToCheckout") }} <i class="fa fa-arrow-right" aria-hidden="true"></i>
    </a>
    {{ LayoutContainer.show("Ceres::BasketPreview.AfterCheckoutButton") }}
</div>

In the Ceres template, the PayPal Express button will be displayed below the normal Go to checkout button.

Changing the payment method subsequently

Every payment plugin can specify whether you can switch the payment method in the My account area after the order has been placed. For this purpose, the two methods isSwitchableTo and isSwitchableFrom can be used.

PayPal/src/Methods/PayPalPaymentMethod.php
<?php

namespace PayPal\Methods;

use PayPal\Services\PaymentService;
use Plenty\Modules\Basket\Contracts\BasketRepositoryContract;
use Plenty\Modules\Payment\Method\Contracts\PaymentMethodService;
use Plenty\Plugin\ConfigRepository;
use Plenty\Modules\Frontend\Contracts\Checkout;
use Plenty\Plugin\Application;

...

    /**
     * Check if it is allowed to switch to this payment method
     *
     * @param int $orderId
     * @return bool
     */
    public function isSwitchableTo($orderId)
    {
        return false;
    }

    /**
     * Check if it is allowed to switch from this payment method
     *
     * @param int $orderId
     * @return bool
     */
    public function isSwitchableFrom($orderId)
    {
        return true;
    }
}

A button will be displayed in the My account area of Ceres next to the respective order in the order history. When clicking the button Change payment method, a list of available payment methods will be shown. When the customer clicks the button a REST call is sent and triggers a method in the OrderHistory.twig template.

Ceres/resources/views/MyAccount/Components/OrderHistory.twig
...

{{ component( "Ceres::MyAccount.Components.ChangePaymentMethod" ) }}

...

                                            <change-payment-method  template="#vue-change-payment-method"
                                                                    :current-order="{{ entry | json_encode() }}"
                                                                    :change-possible="{{ services.order.allowPaymentMethodSwitchFrom(paymentMethodId, entry.order.id) | json_encode() }}"
                                                                    :allowed-payment-methods="{{ services.order.getPaymentMethodListForSwitch(paymentMethodId, entry.order.id) | json_encode() }}">
                                            </change-payment-method>

...

The getPaymentMethodListForSwitch method is located in the file OrderService.php of the plugin IO. Here, a new plugin interface is used:

IO/src/Services/OrderService.php
<?php

namespace IO\Services;

use IO\Models\LocalizedOrder;
use Plenty\Modules\Frontend\PaymentMethod\Contracts\FrontendPaymentMethodRepositoryContract;
use Plenty\Modules\Order\Contracts\OrderRepositoryContract;
use Plenty\Modules\Order\Property\Contracts\OrderPropertyRepositoryContract;
use Plenty\Modules\Order\Property\Models\OrderProperty;
use Plenty\Modules\Order\Property\Models\OrderPropertyType;
use Plenty\Modules\Payment\Method\Contracts\PaymentMethodRepositoryContract;
use IO\Builder\Order\OrderBuilder;
use IO\Builder\Order\OrderType;
use IO\Builder\Order\OrderOptionSubType;
use IO\Builder\Order\AddressType;
use IO\Constants\OrderStatusTexts;
use Plenty\Repositories\Models\PaginatedResult;
use IO\Constants\SessionStorageKeys;

...

    /**
     * List all payment methods available for switch in MyAccount
     * @param int $currentPaymentMethodId
     * @param int $orderId
     * @return \Plenty\Modules\Payment\Method\Models\PaymentMethod[]
     */
    public function getPaymentMethodListForSwitch($currentPaymentMethodId = 0, $orderId = null)
    {
        return $this->frontendPaymentMethodRepository->getCurrentPaymentMethodsListForSwitch($currentPaymentMethodId, $orderId);
    }

...

}

As mentioned before, a Vue component is used for changing the payment method. This component consists of a Twig template and a Vue.js file.

Ceres/resources/views/MyAccount/Components/ChangePaymentMethod.twig
<script type="x/template" id="vue-change-payment-method">

    <button v-if="changePossible" class="btn btn-primary btn-block" @click="openPaymentChangeModal()">
        {{ trans("Ceres::Template.myAccountChangePayment") }}
    </button>

    <div v-if="!changePossible" class="payment-align-center">
        <h1 class="h4 text-mute font-italic payment-change-text">{{ trans("Ceres::Template.myAccountChangePaymentNotAllowed") }}</h1>
    </div>

    
    <div v-el:change-payment-modal>
        <div class="modal fade" data-backdrop="static">
            <div class="modal-dialog" role="dialog">
                <div class="modal-content">
                    <div class="modal-header">
                        <button type="button" class="close" data-dismiss="modal" aria-label="Close" @click="closeModal()">
                            <span aria-hidden="true">×</span>
                        </button>
                        <h4 class="modal-title">{{ trans("Ceres::Template.myAccountChooseNewPayment") }}</h4>
                    </div>
                    <div class="modal-body form-horizontal">

                        <div class="cmp cmp-method-list m-b-3">
                            <ul class="method-list">
                                <li v-for="paymentProvider in allowedPaymentMethods" class="method-list-item" data-id="${paymentProvider.id}">
                                    <input
                                            type="radio"
                                            id="paymentMethod${ _uid }${ paymentProvider.id }"
                                            name="MethodOfPaymentID_${ currentOrder.order.id }"
                                            v-model="paymentMethod"
                                            :value="paymentProvider.id"
                                    >
                                    <label for="paymentMethod${ _uid }${ paymentProvider.id }">
                                        <div class="icon">
                                            <div class="square-container">
                                                <div class="square-inner center-xy">
                                                    <img alt="${ paymentProvider.name }" :src="paymentProvider.icon">
                                                </div>
                                            </div>
                                        </div>
                                        <div class="content">
                                        ${ paymentProvider.name }
                                            <div>
                                                <small>
                                                    ${ paymentProvider.fee | currency }
                                                </small>
                                            </div>
                                            <div>
                                                <small>
                                                    ${ paymentProvider.description }
                                                </small>
                                            </div>
                                        </div>
                                    </label>
                                </li>
                            </ul>
                        </div>

                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-secondary" data-dismiss="modal">
                            <i class="fa fa-times" aria-hidden="true"></i> {{ trans("Ceres::Template.generalCancel") }}
                        </button>
                        <button type="button" :class="{'disabled': isPending}" class="btn btn-primary" @click="changePaymentMethod()">
                            <i class="fa fa-check" aria-hidden="true"></i> {{ trans("Ceres::Template.generalChange") }}
                        </button>
                    </div>
                </div>
            </div>
        </div>
    </div>
    

</script>
Ceres/resources/js/src/app/components/myAccount/ChangePaymentMethod.js
const ModalService        = require("services/ModalService");
const ApiService          = require("services/ApiService");

Vue.component("change-payment-method", {

    props: [
        "template",
        "currentOrder",
        "allowedPaymentMethods",
        "changePossible"
    ],

    data()
    {
        return {
            changePaymentModal: {},
            paymentMethod: 0,
            isPending: false
        };
    },

    created()
    {
        this.$options.template = this.template;
    },

    /**
     * Initialize the change payment modal
     */
    ready()
    {
        this.changePaymentModal = ModalService.findModal(this.$els.changePaymentModal);
    },

    methods:
    {
        checkChangeAllowed()
        {
            ApiService.get("/rest/io/order/payment", {orderId: this.currentOrder.order.id, paymentMethodId: this.paymentMethod})
                .done(response =>
                {
                    this.changePossible = response;
                })
                .fail(() =>
                {
                    this.changePossible = false;
                });
        },

        openPaymentChangeModal()
        {
            this.changePaymentModal.show();
        },

        getPaymentStateText(paymentStates)
        {
            for (const paymentState in paymentStates)
            {
                if (paymentStates[paymentState].typeId == 4)
                {
                    return Translations.Template["paymentStatus_" + paymentStates[paymentState].value];
                }
            }

            return "";
        },

        closeModal()
        {
            this.changePaymentModal.hide();
            this.isPending = false;
        },

        updateOrderHistory(updatedOrder)
        {
            document.getElementById("payment_name_" + this.currentOrder.order.id).innerHTML = updatedOrder.paymentMethodName;
            document.getElementById("payment_state_" + this.currentOrder.order.id).innerHTML = this.getPaymentStateText(updatedOrder.order.properties);

            this.checkChangeAllowed();

            this.closeModal();
        },

        changePaymentMethod()
        {
            this.isPending = true;

            ApiService.post("/rest/io/order/payment", {orderId: this.currentOrder.order.id, paymentMethodId: this.paymentMethod})
                .done(response =>
                {
                    document.dispatchEvent(new CustomEvent("historyPaymentMethodChanged", {detail: {oldOrder: this.currentOrder, newOrder: response}}));

                    this.updateOrderHistory(response);
                })
                .fail(() =>
                {
                });
        }
    }

});

Reinitialising the payment method

After having changed the payment method, the new payment method has to be initialised. To do so, the plugin needs two containers: one for a button in the My account area and another one for the script that manages it. These containers need to be set up first.

PayPal/plugin.json
...

{
    "key":"PayPal\\Providers\\DataProvider\\PayPalReinitializePayment",
    "name":"PayPal Reinitialize Payment",
    "description":"Display the PayPal Button after the Payment changed to PayPal"
},
{
    "key":"PayPal\\Providers\\DataProvider\\PayPalReinitializePaymentScript",
    "name":"PayPal Reinitialize Payment Script",
    "description":"A Script for displaying the PayPal Button after the Payment changed to PayPal"
}

...

The functionality for the button is constructed in two parts: the button needs to be rendered and the script needs to be provided.

PayPal/src/Providers/DataProvider/PayPalReinitializePayment.php
<?php

namespace PayPal\Providers\DataProvider;

use PayPal\Helper\PaymentHelper;
use Plenty\Plugin\Templates\Twig;

class PayPalReinitializePayment
{
    public function call(Twig $twig, $arg):string
    {
        $paymentHelper = pluginApp(PaymentHelper::class);
        $paymentMethodId = $paymentHelper->getPayPalMopIdByPaymentKey(PaymentHelper::PAYMENTKEY_PAYPAL);
        return $twig->render('PayPal::PayPalReinitializePayment', ["order" => $arg[0], "paymentMethodId" => $paymentMethodId]);
    }
}
PayPal/src/Providers/DataProvider/PayPalReinitializePaymentScript.php
<?php

namespace PayPal\Providers\DataProvider;

use Plenty\Plugin\Templates\Twig;
use PayPal\Helper\PaymentHelper;

class PayPalReinitializePaymentScript
{

    public function call(Twig $twig):string
    {
        $paymentHelper = pluginApp(PaymentHelper::class);
        $pp = $paymentHelper->getPayPalMopIdByPaymentKey(PaymentHelper::PAYMENTKEY_PAYPAL);
        return $twig->render('PayPal::PayPalReinitializePaymentScript', ['mopIds' => ['pp' => $pp]]);
    }
}

The button has to be displayed in the My account area for every order with an applicable payment method. In addition, the button is displayed on the Order confirmation page.

PayPal/resources/views/PayPalReinitializePayment.twig
{% set paymentId = 0 %}
{% set paidStatus = '' %}

{% set properties = order.properties %}

{% for property in properties %}
    {% if property.typeId == 3 %}
        {% set paymentId = property.value %}
    {% endif %}
    {% if property.typeId == 4 %}
        {% set paidStatus = property.value %}
    {% endif %}
{% endfor %}
{% if (paymentId == paymentMethodId) and (paidStatus != 'fullyPaid') %}
    {% set display = "block" %}
{% else %}
    {% set display = "none" %}
{% endif %}

{% if services.template.isCurrentTemplate('tpl.my-account') %}

    <button id="reinitPayPalPlus-{{order.id}}" class="btn btn-primary btn-block" @click="" data-toggle="modal" data-target="#payPalPlusWall" :disabled="" style="display: {{ display }}; margin-top: 0.5rem">
        {{ trans("PayPal::PayPal.myAccountReinitPayment") }}
    </button>

{% elseif services.template.isCurrentTemplate('tpl.confirmation') %}

    <div id="reinitPayPalPlus-{{order.id}}" class="row con-reinit" style="display: {{ display }};">
        <strong class="col-xs-6 col-sm-5"></strong>
        <span class="col-xs-6 col-sm-7">
            <a class="payment-confirmation-btn" @click="" data-toggle="modal" data-target="#payPalPlusWall" :disabled="">
                <span>{{ trans("PayPal::PayPal.myAccountReinitPayment") }}</span>
            </a>
        </span>
    </div>

{% endif %}
PayPal/resources/views/PayPalReinitializePaymentScript.twig
<script type="text/javascript">
    $(function () {
        $("[id^='reinitPayPal-']").click(function () {
            var orderId = $(this).attr('id').split('-')[1];
            $.get("/payment/payPal/payOrderNow/"+orderId, function(data)
            {
                window.location = data;
            });
        });
    });

    document.addEventListener('historyPaymentMethodChanged', e => {
        for(let property in e.detail.newOrder.order.properties){
        if(e.detail.newOrder.order.properties[property].typeId === 3){
            if (e.detail.newOrder.order.properties[property].value == {{ mopIds.pp }}) {
                document.getElementById("reinitPayPal-" + e.detail.oldOrder.order.id).style.display = "block";
            }else {
                document.getElementById("reinitPayPal-" + e.detail.oldOrder.order.id).style.display = "none";
            }
        }
    }
    });
</script>

Creating event procedures

The src/procedures folder contains the RefundEventProcedure.php file and other files that are used to integrate event procedures for the payment method in plentymarkets. For detailed information about event procedures, refer to New event procedure. Under Procedures, the procedure type Plugins is available. All event procedures that are registered in a plugin, will be listed under the Plugins procedure type.

PayPal/src/events/RefundEventProcedure.php
<?php

namespace PayPal\Procedures;

use Plenty\Modules\Order\Models\Order;
use Plenty\Modules\Payment\Models\Payment;
use Plenty\Modules\EventProcedures\Events\EventProceduresTriggered;
use Plenty\Modules\Plugin\Libs\Contracts\LibraryCallContract;
use Plenty\Modules\Payment\Contracts\PaymentRepositoryContract;
use PayPal\Services\PaymentService;
use PayPal\Helper\PaymentHelper;

/**
 * Class RefundEventProcedure
 * @package PayPal\Procedures
 */
class RefundEventProcedure
{
    /**
     * @param EventProceduresTriggered $eventProceduresTriggered
     * @param PaymentService $paymentService
     * @param PaymentRepositoryContract $paymentContract
     */
    public function run(EventProceduresTriggered $eventProceduresTriggered,
                        PaymentService $paymentService,
                        PaymentRepositoryContract $paymentContract)
    {
        /** @var Order $order */
        $order = $eventProceduresTriggered->getOrder();
        /** @var Payment $payment */
        $payment = $paymentContract->getPaymentsByOrderId($order->id);
        $paymentData = array(   'currency' => $payment->currency,
                                'total'    => $payment->amount);
        $result = $paymentService->refundPayment($paymentData);
    }
}

The event procedure must be registered in the ServiceProvider.

PayPal/src/providers/PayPalServiceProvider.php
<?php

namespace PayPal\Providers;

use Plenty\Modules\EventProcedures\Services\Entries\ProcedureEntry;
use Plenty\Modules\EventProcedures\Services\EventProceduresService;

...

use PayPal\Procedures\RefundEventProcedure;

...

/**
 * Class PayPalServiceProvider
 * @package PayPal\Providers
 */
class PayPalServiceProvider extends ServiceProvider
{

      /**
       * Register the route service provider and bind event procedures
       */
      public function register()
      {
          $this->getApplication()->register(PayPalRouteServiceProvider::class);
          $this->getApplication()->bind(RefundEventProcedure::class);
      }

...

            // Register PayPal Refund Event Procedure
            $eventProceduresService->registerProcedure('plentyPayPal', ProcedureEntry::PROCEDURE_GROUP_ORDER,
            [   'de' => 'Rückzahlung der PayPal-Zahlung',
                'en' => 'Refund the PayPal payment'],
            '\PayPal\Procedures\RefundEventProcedure@run');

...