A basic payment plugin

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.

Step 1: Installing the template

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.

Step 2: Creating the plugin files

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

Step 3: Filling the source files

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.

Code for the plugin.json

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"
    ],
}

Code for the ServiceProvider

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');
                }
            });
    }
}

Code for the Helper

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';
    }
}

Code for the Migration

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'
        ]);
	}
}
    

Code for the PaymentMethod

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;
    }
}

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.

Code for the Assistant

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;
    }

}
    
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;
    }
}
    

Step 4: Setting up a shipping profile

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.

Creating the shipping provider

  1. Go to Setup » Orders » Shipping » Settings » Tab: Shipping service provider.
  2. Click on New.
    → A new line will be added at the bottom of the overview.
  3. Enter the name.
    → Enter both front end and back end names, e.g. Picked up by customer.
  4. Select Selbstabholer (Picked up by customer) from the Shipping service provider drop-down menu.
  5. Save the settings.

Creating the shipping profile

  1. Go to Setup » Orders » Shipping » Settings » Tab: Shipping profiles.
  2. Click on New.
  3. Select the shipping service provider that you have just created from the drop-down menu.
  4. Enter the name.
    → Enter both front end and back end names, e.g. Pickup.
  5. Activate the client (store).
  6. Activate the order referrer Client (store).
  7. Save the settings.
    → The shipping profile is created. The ID is shown in the overview.

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.

Is this article helpful?

 

Thank you for your Feedback

you can close this field now!