In this tutorial, we will develop a basic plugin to integrate a payment method in our template plugin. We want to enable our customers to select the payment method Pay upon pickup in the checkout after choosing the shipping method Picked up by customer. The payment method Pay upon pickup allows the customers paying cash when picking up an item.
If you haven't already set up a template in your plentymarkets system, do it before you start developing the payment plugin. In this way, you have your test environment ready and can directly check your coding output.
Our plugin is of the payment type and integrates with the Ceres template, i.e. our plugin consists of core features saved in the src folder, as well as the plugin.json. This basic plugin does not require any design features. In more complex payment plugins, images and javascript files are saved in the resources folder.
PluginPayUponPickup/ ├── meta/ │ ├── documents │ | └── changelog_de.md │ | └── changelog_en.md │ | └── support_contact_de.md │ | └── support_contact_en.md │ | └── user_guide_de.md │ | └── user_guide_en.md │ │ │ └── images │ └── icon_author_md.png │ └── icon_author_sm.png │ └── icon_author_xs.png │ └── icon_plugin_md.png │ └── icon_plugin_sm.png │ └── icon_plugin_xs.png │ └── preview_0.png │ ├── resources/ │ | └── images/ │ │ │ └── icon.png │ │ │ └── backend_icon.svg │ | └── lang/ │ │ │ └── de/ │ │ │ │ └── assistant.properties │ │ │ │ └── MultilingualismConfig.properties │ │ │ │ └── PaymentMethod.properties │ │ │ └── en/ │ │ │ └── assistant.properties │ │ │ └── MultilingualismConfig.properties │ │ │ └── PaymentMethod.properties │ | └── views/ │ │ │ └── Icon.twig │ | └── lib/ │ │ └── //Store files to communicate with an external sdk, for example. │ │ ├── src/ │ ├── Assistants/ │ │ └── DataSources/ │ │ │ └── AssistantDataSource.php │ │ └── SettingsHandler/ │ │ │ └── PayUponPickupSettingsHandler.php │ | └── PayUponPickupAssistant.php │ │ │ ├── Controllers/ │ │ └── SettingsController.php │ │ │ ├── Extensions/ │ │ └── PayUponPickupTwigServiceProvider.php │ │ │ ├── Helper/ │ │ └── PayUponPickupHelper.php │ │ │ ├── Methods/ │ │ └── PayUponPickupPaymentMethod.php │ │ │ ├── Migrations/ │ │ └── CreatePaymentMethod.php │ │ │ ├── Models/ │ │ └── Settings.php │ │ └── ShippingCountrySettings.php │ │ │ ├── Providers/ │ │ └── PayUponPickupServiceProvider.php │ │ │ └── Services/ │ └── SessionStorageService.php │ └── SettingService.php │ └── plugin.json // plugin information └── translation.json // multilingual information
We start by creating the plugin.json
file. We will also need a ServiceProvider
, a Helper
, a PaymentMethod
and an Assistant
in the src folder of our plugin. Create these files and copy the code examples.
PayUponPickup/plugin.json
{ "name" : "PayUponPickup", "description" : "Pay upon pickup payment method", "author" : "Your name", "keywords" : ["plentymarkets", "payment method", "plugin"], "type" : "payment", "namespace" : "PayUponPickup", //Require is not needed in our case, but you set plugin versions which are needed //for building the plugin here. "require": { "IO":">=4.1.1" , "Ceres":">=4.1.1" }, //If you need some extra libraries you can set them as dependencies. "dependencies":{ "guzzlehttp/guzzle":"6.*" }, "serviceProvider" : "PayUponPickup\\Providers\\PayUponPickupServiceProvider", "runOnBuild": [ "PayUponPickup\\Migrations\\CreatePaymentMethod" ], }
payment
type. A list of keywords describing the plugin is entered under keywords
.
For more information visit the plugin information page.
PayUponPickup/src/Providers/PayUponPickupServiceProvider.php
namespace PayUponPickup\Providers; use Plenty\Plugin\ServiceProvider; use Plenty\Plugin\Events\Dispatcher; use Plenty\Modules\Payment\Events\Checkout\ExecutePayment; use Plenty\Modules\Payment\Events\Checkout\GetPaymentMethodContent; use Plenty\Modules\Basket\Events\Basket\AfterBasketCreate; use Plenty\Modules\Basket\Events\Basket\AfterBasketChanged; use Plenty\Modules\Basket\Events\BasketItem\AfterBasketItemAdd; use Plenty\Modules\Payment\Method\Contracts\PaymentMethodContainer; use PayUponPickup\Helper\PayUponPickupHelper; use PayUponPickup\Methods\PayUponPickupPaymentMethod; use Plenty\Modules\Wizard\Contracts\WizardContainerContract; use PayUponPickup\Assistants\PayUponPickupAssistant; /** * Class PayUponPickupServiceProvider * @package PayUponPickup\Providers */ class PayUponPickupServiceProvider extends ServiceProvider { public function register() { } /** * Boot additional services for the payment method. * * @param PayUponPickupHelper $paymentHelper * @param PaymentMethodContainer $payContainer * @param Dispatcher $eventDispatcher */ public function boot( PayUponPickupHelper $paymentHelper, PaymentMethodContainer $payContainer, Dispatcher $eventDispatcher ) { // Register the Pay upon pickup payment method in the payment method container. $payContainer->register('plenty_payuponpickup::PAYUPONPICKUP', PayUponPickupPaymentMethod::class, [AfterBasketChanged::class, AfterBasketItemAdd::class, AfterBasketCreate::class] ); // Register the assistant for the payment method. pluginApp(WizardContainerContract::class)->register('payment-payUponPickupAssistant-assistant', PayUponPickupAssistant::class); // Listen for the event that gets the payment method content. $eventDispatcher->listen(GetPaymentMethodContent::class, function (GetPaymentMethodContent $event) use ($paymentHelper) { // In every event it is absolutely necessary to check if this is your own payment method if ($event->getMop() == $paymentHelper->getPaymentMethod()) { $event->setValue(''); $event->setType('continue'); } }); // Listen for the event that executes the payment. $eventDispatcher->listen(ExecutePayment::class, function (ExecutePayment $event) use ($paymentHelper) { if ($event->getMop() == $paymentHelper->getPaymentMethod()) { $event->setValue('<h1>Pay upon pickup<h1>'); $event->setType('htmlContent'); } }); } }
use
all necessary aspects for our plugin, such as events for payments and the shopping cart, the payment container, the helper and the payment method.boot
function. This function boots additional services for the payment method.
The function contains the logic for creating the ID of the payment method, registering the payment method, registering the assistant and listening for events.
PayUponPickup/src/Helper/PayUponPickupHelper.php
namespace PayUponPickup\Helper; use Plenty\Modules\Payment\Method\Contracts\PaymentMethodRepositoryContract; use Plenty\Modules\Payment\Method\Models\PaymentMethod; /** * Class PayUponPickupHelper * * @package PayUponPickup\Helper */ class PayUponPickupHelper { /** * @var PaymentMethodRepositoryContract $paymentMethodRepository */ private $paymentMethodRepository; /** * PayUponPickupHelper constructor. * * @param PaymentMethodRepositoryContract $paymentMethodRepository */ public function __construct(PaymentMethodRepositoryContract $paymentMethodRepository) { $this->paymentMethodRepository = $paymentMethodRepository; } /** * Load the ID of the payment method for the given plugin key. * Return the ID for the payment method. * * @return string|int */ public function getPaymentMethod() { $paymentMethods = $this->paymentMethodRepository->allForPlugin('plenty_payuponpickup'); if (!is_null($paymentMethods)) { foreach ($paymentMethods as $paymentMethod) { if ($paymentMethod->paymentKey == 'PAYUPONPICKUP') { return $paymentMethod->id; } } } return 'no_paymentmethod_found'; } }
constructor
is loaded only once - when initialising the class
. All required default values are loaded and executed with __construct
. In our example, this function is used to load the ID of the payment method based on an array.$paymentMethodData
array includes the plugin key, the payment key and the name of the payment method. This information is used to generate a unique ID. When using a payment plugin for the first time, the ID of the payment method is created and saved in the plentymarkets data base.
PayUponPickup/src/Migrations/CreatePaymentMethod.php
namespace PayUponPickup\Migrations; use Plenty\Modules\Payment\Method\Contracts\PaymentMethodRepositoryContract; /** * Class CreatePaymentMethod */ class CreatePaymentMethod { /** * @var PaymentMethodRepositoryContract */ private $paymentMethodRepositoryContract; /** * CreatePaymentMethod constructor. * * @param PaymentMethodRepositoryContract $paymentMethodRepositoryContract */ public function __construct(PaymentMethodRepositoryContract $paymentMethodRepositoryContract) { $this->paymentMethodRepositoryContract = $paymentMethodRepositoryContract; } /** * The run method will register the payment method when the migration runs. */ public function run() { $this->paymentMethodRepositoryContract->createPaymentMethod([ 'pluginKey' => 'sofort', 'paymentKey' => 'SOFORT', 'name' => 'SOFORT' ]); } }
PayUponPickup/src/Methods/PayUponPickupPaymentMethod.php
namespace PayUponPickup\Methods; use Plenty\Plugin\ConfigRepository; use Plenty\Modules\Payment\Method\Services\PaymentMethodBaseService; use Plenty\Modules\Basket\Contracts\BasketRepositoryContract; use Plenty\Modules\Basket\Models\Basket; /** * Class PayUponPickupPaymentMethod * @package PayUponPickup\Methods */ class PayUponPickupPaymentMethod extends PaymentMethodBaseService { /** @var BasketRepositoryContract */ private $basketRepo; /** @var SettingsService */ private $settings; /** @var Checkout */ private $checkout; /** * PayUponPickupPaymentMethod constructor. * @param BasketRepositoryContract $basketRepo * @param SettingsService $settingsService * @param Checkout $checkout */ public function __construct( BasketRepositoryContract $basketRepo, SettingsService $settingsService, Checkout $checkout ) { $this->basketRepo = $basketRepo; $this->settings = $settingsService; $this->checkout = $checkout; } /** * Check if the payment method is active. * Return true if the payment method is active, else return false. * * @return bool */ public function isActive(): bool { /** * In our assistant, we let the user decide in which shipping countries the payment method * is allowed, therefore we have to check it here. */ if (!in_array($this->checkout->getShippingCountryId(), $this->settings->getShippingCountries())) { return false; } return true; } /** * Get the name of the payment method. * * @param string $lang * @return string */ public function getName(string $lang = 'de'): string { /** @var Translator $translator */ $translator = pluginApp(Translator::class); /** * Here we use the translator class to allow multilingualism. Every variable * of the translator can be found and configured in CMS » Multilingualism. */ return $translator->trans('PayUponPickup::PaymentMethod.paymentMethodName', [], $lang); } /** * Return an additional payment fee for the payment method. * * @return float */ public function getFee(): float { return 0.00; } /** * Get the path of the icon. * * @return string */ public function getIcon(string $lang): string { /** * Here we want to get the logo, but we let our user decide in the assistant if * he wants a custom logo or the basic logo. Therefore, we have to get our logo settings * and either return the uploaded image url or the default image. */ if ($this->settings->getSetting('logo') == 1) { return $this->settings->getSetting('logoUrl'); } elseif ($this->settings->getSetting('logo') == 2) { $app = pluginApp(Application::class); $icon = $app->getUrlPath('payuponpickup').'/images/icon.png'; return $icon; } return ''; } /** * Get the description of the payment method. * * @return string */ public function getDescription(string $lang): string { /** * Here we want to use the frontend session to detect the language and * return the description of a payment method. */ /** @var FrontendSessionStorageFactoryContract $session */ $session = pluginApp(FrontendSessionStorageFactoryContract::class); $lang = $session->getLocaleSettings()->language; /** * Here we use the translator class to allow multilingualism. Every variable of * the translator can be found and configured in CMS » Multilingualism. */ /** @var Translator $translator */ $translator = pluginApp(Translator::class); return $translator->trans('PayUponPickup::PaymentMethod.paymentMethodDescription', [], $lang); } /** * Return an URL with additional information shown in the frontend about the payment method * in the corresponding language. * * @param string $lang * @return string */ public function getSourceUrl(string $lang): string { return ''; } /** * Check if it is allowed to switch to this payment method after the order has been placed. * * @return bool */ public function isSwitchableTo(): bool { return false; } /** * Check if it is allowed to switch from this payment method to another after the order has been placed. * * @return bool */ public function isSwitchableFrom(): bool { return false; } /** * Check if this payment method should be searchable in the back end. * * @return bool */ public function isBackendSearchable(): bool { return true; } /** * Check if this payment method should be active in the back end. * * @return bool */ public function isBackendActive(): bool { return true; } /** * Get the name for the back end. * * @param string $lang * @return string */ public function getBackendName(string $lang): string { return $this->getName($lang); } /** * Check if this payment method can handle subscriptions. * * @return bool */ public function canHandleSubscriptions(): bool { return true; } /** * Return the icon for the back end, shown in the payments UI. * * @return string */ public function getBackendIcon(): string { $app = pluginApp(Application::class); $icon = $app->getUrlPath('payuponpickup').'/images/backend_icon.svg'; return $icon; } }
isActive
function checks whether the payment method is active. It also checks the shipping country, making our payment method only available for the shipping country which has been selected in the assistant.Your payment method needs to extend the PaymentMethodBaseService which contains the following methods:
isActive()
: Check if the payment method is active for the frontend or not. Use settings to decide if the method is active. Return "true" if active, "false" if not.
getName(string $lang)
: Return the payment method name for the frontend as a string.
getFee()
: If required you can add an additional fee in the checkout. Return as a float value.
getIcon(string $lang)
: Return an icon for the frontend, shown within the payment methods list. Return the icon path as a string.
getDescription(string $lang)
: Payment method description for the frontend, also shown within the payment methods list.
getSourceUrl(string $lang)
: If you have to provide additional information on an extra page you can return an url to this page. The link will be shown within the payment methods list.
isSwitchableTo()
: Determine if it is possible to switch to this payment method after the order has been placed. You have to make sure that it is possible to reinitialise the payment process based on the order. Default is false.
isSwitchableFrom()
: Determine if it is possible to switch from this payment method to another after the order has been placed. Thus, customers are given the possibility to change the payment method. Default is false.
isBackendSearchable()
: Determine if the payment method is available in the searching drop-down menus in the back end. Default is true.
isBackendActive()
: Determine if it is possible to select the payment method in the back end for an already existing order or when an order is created manually. You have to make sure that your payment method can handle this. Default is true.
getBackendName(string $lang)
: Return the payment method name for the back end as string.
canHandleSubscriptions()
: Determine if the payment method can handle recurring payments.
getBackendIcon()
: Return an icon for the back end, shown in the payment ui. The icon has to be provided as an "svg" file. Return the path to the icon as a string.
PayUponPickup/src/Assistants/PayUponPickupAssistant.php
namespace PayUponPickup\Assistants; use PayUponPickup\Assistants\SettingsHandlers\PayUponPickupAssistantSettingsHandler; use Plenty\Modules\System\Contracts\WebstoreRepositoryContract; use Plenty\Modules\Wizard\Services\WizardProvider; use Plenty\Plugin\Application; class PayUponPickupAssistant extends WizardProvider { /** * @var WebstoreRepositoryContract */ private $webstoreRepository; /** * @var Array */ private $webstoreValues; public function __construct( WebstoreRepositoryContract $webstoreRepository ) { $this->webstoreRepository = $webstoreRepository; } /** * In this method we define the basic settings and the structure of the assistant in an array. * Here we have to define aspects like the topic, settings handler, steps and form elements. */ protected function structure() { return [ /** Use translate keys for multilingualism. */ "title" => 'assistant.assistantTitle', "shortDescription" => 'assistant.assistantShortDescription', "iconPath" => $this->getIcon(), /** Add our settings handler class */ "settingsHandlerClass" => PayUponPickupAssistantSettingsHandler::class, "translationNamespace" => "PayUponPickup", "key" => "payment-payUponPickupAssistant-assistant", /** The topic needs to be payment. */ "topics" => ["payment"], "priority" => 990, "options" => [ "config_name" => [ "type" => 'select', 'defaultValue' => $this->getMainWebstore(), /** We need a list of all webstores to configure each individually. */ "options" => [ "name" => 'assistant.storeName', 'required' => true, 'listBoxValues' => $this->getWebstoreListForm(), ], ], ], /** Define steps for the assistant. */ "steps" => [ "stepOne" => [ "title" => "assistant.stepOneTitle", "sections" => [ [ "title" => 'assistant.shippingCountriesTitle', "description" => 'assistant.shippingCountriesDescription', /** * Define form elements for the first step, in our case * a selection of available delivery countries. */ "form" => [ "shippingCountries" => [ 'type' => 'checkboxGroup', 'defaultValue' => [], 'options' => [ 'name' => 'assistant.shippingCountries', 'checkboxValues' => $this->getCountriesListForm(), ], ], ], ], ], ], /** Define as many steps as needed */ "stepTwo" => [ /** ..... */ ], ] ]; } /** * We need an icon for our assistant, so we just return the basic icon as string. You may * want to return different icons depending on the language of the back end user. */ private function getIcon() { $app = pluginApp(Application::class); $icon = $app->getUrlPath('PayUponPickup').'/images/icon.png'; return $icon; } /** * We use this method to create a drop-down menu with all webstores * to configure our assistant for each client individually. */ private function getWebstoreListForm() { if ($this->webstoreValues === null) { $webstores = $this->webstoreRepository->loadAll(); /** @var Webstore $webstore */ foreach ($webstores as $webstore) { /** We need a caption and a value because it is a drop-down menu. */ $this->webstoreValues[] = [ "caption" => $webstore->name, "value" => $webstore->storeIdentifier, ]; } /** Sort the array for better usability. */ usort($this->webstoreValues, function ($a, $b) { return ($a['value'] <=> $b['value']); }); } return $this->webstoreValues; } }
PayUponPickupAssistant
class you have to extend the WizardProvider
and define a structure for you assistant with all available options. You also need some extra methods. In this case we are using getWebstoreListForm
to return a list of all webstores. This list of webstores is needed to individually configure the assistant for each webstore.
getCountriesListForm
will return a country list so you can select different delivery countries.
PayUponPickup/src/Assistants/SettingsHandlers/PayUponPickupAssistantSettingsHandler.php
namespace PayUponPickup\Assistants\SettingsHandlers; use PayUponPickup\Services\SettingsService; use Plenty\Modules\Plugin\Contracts\PluginLayoutContainerRepositoryContract; use Plenty\Modules\Wizard\Contracts\WizardSettingsHandler; class PayUponPickupAssistantSettingsHandler implements WizardSettingsHandler { /** * This method is called after you click on complete in the last step of an assistant. * We get an array of the data and validate it before saving it. * * @param array $parameter * @return bool */ public function handle(array $parameter) { $data = $parameter['data']; // Get the form data $webstoreId = $data['config_name']; /** Validate the webstore ID. */ if (!is_numeric($webstoreId) || $webstoreId <= 0) { $webstoreId = $this->getWebstore($parameter['optionId'])->storeIdentifier; } /** Save the settings. */ $this->saveSettings($webstoreId, $data); /** Create the container links so the user does not have to do this. */ $this->createContainer($webstoreId, $data); return true; } /** * In this method you should implement things such as validate, throw exceptions and * define some fallback values to prevent errors. * * @param int $webstoreId * @param array $data */ private function saveSettings($webstoreId, $data) { /** We validate the form data and define some fallback values. */ $settings = [ 'name' => $data['name'] ?? '', 'logo' => $data['logo'] ? 1 : 2, 'logoUrl' => $data['logo_url'] ?? '', /** ...... */ ]; /** @var SettingsService $settingsService */ $settingsService = pluginApp(SettingsService::class); $settingsService->saveSettings($settings); } /** * Here we create the container links based on the form data, * e.g. show the icon in the footer of the webshop. * @param int $webstoreId * @param array $data */ private function createContainer($webstoreId, $data) { /** We have the webstore ID, but we need the whole webstore. */ $webstore = $this->getWebstore($webstoreId); /** We need the plugin ID of our payment method. */ $payUponPickupPlugin = $this->getPayUponPickupPlugin($webstoreId); /** In order to create the container links we also need the plugin ID of Ceres. */ $ceresPlugin = $this->getCeresPlugin($webstoreId); if (($webstore && $webstore->pluginSetId) && $payUponPickupPlugin !== null && $ceresPlugin !== null) { /** @var PluginLayoutContainerRepositoryContract $pluginLayoutContainerRepo */ $pluginLayoutContainerRepo = pluginApp(PluginLayoutContainerRepositoryContract::class); $containerListEntries = []; /** * In our assistant the user can choose if the icon of the payment method * should be in the footer or not. That's why we either add a container * connection or remove it. */ if (isset($data['paymentMethodIcon']) && $data['paymentMethodIcon']) { $containerListEntries[] = $this->createContainerDataListEntry( $webstoreId, 'Ceres::Homepage.PaymentMethods', 'PayUponPickup\Providers\Icon\IconProvider' ); } else { $pluginLayoutContainerRepo->removeOne( $webstore->pluginSetId, 'Ceres::Homepage.PaymentMethods', 'PayUponPickup\Providers\Icon\IconProvider', $ceresPlugin->id, $payUponPickupPlugin->id ); } $pluginLayoutContainerRepo->addNew($containerListEntries, $webstore->pluginSetId); } } /** * This method is used to return an array which is needed to create a container link. * * @param int $webstoreId * @param string $containerKey * @param string $dataProviderKey * @return array */ private function createContainerDataListEntry($webstoreId, $containerKey, $dataProviderKey) { $webstore = $this->getWebstore($webstoreId); $payUponPickupPlugin = $this->getPayUponPickupPlugin($webstoreId); $ceresPlugin = $this->getCeresPlugin($webstoreId); $dataListEntry = []; $dataListEntry['containerKey'] = $containerKey; $dataListEntry['dataProviderKey'] = $dataProviderKey; $dataListEntry['dataProviderPluginId'] = $payUponPickupPlugin->id; $dataListEntry['containerPluginId'] = $ceresPlugin->id; $dataListEntry['pluginSetId'] = $webstore->pluginSetId; $dataListEntry['dataProviderPluginSetEntryId'] = $payUponPickupPlugin->pluginSetEntries[0]->id; $dataListEntry['containerPluginSetEntryId'] = $ceresPlugin->pluginSetEntries[0]->id; return $dataListEntry; } }
handle
is called with an array of data which contains the assistant form data.
We have to get a few things done in the plentymarkets back end, before our customers can actively use the payment method. We have to set up a suitable shipping profile.
And finally, we deploy the plugin in a plugin set. The payment method Pay upon pickup is displayed in the checkout. Our customers can now select this payment method for the Picked up by customer shipping method and pay in cash when picking up an item.