Important

You are browsing the documentation for version 3.1 of OroCommerce, OroCRM and OroPlatform, which is no longer maintained. Read version 5.1 (the latest LTS version) of the Oro documentation to get up-to-date information.

See our Release Process documentation for more information on the currently supported and upcoming releases.

Payment Methods

This topic describes how to add a custom payment method to your OroCommerce-based store.

It is recommended to manage payment methods through integrations. Therefore, to create a new payment method:

  • Implement an integration for a payment method

  • Implement a payment method itself

As an example, let us implement a collect on delivery (cash on delivery, COD) payment option. This is a simple method that does not utilize external services (like credit card payment interfaces) and requires just the minimum set of options to operate. Thus, at the end of the topic, you will have the understanding of what steps are necessary to add a workable payment method and the basic template that you can further extend when the need arises.

Create a Bundle

First, create and enable the CollectOnDeliveryBundle bundle for your payment method as described in the How to create a new bundle topic:

  1. In the /src/ACME/Bundle/CollectOnDeliveryBundle/ directory of your application, create class ACMECollectOnDeliveryBundle.php:

 1<?php
 2
 3namespace ACME\Bundle\CollectOnDeliveryBundle;
 4
 5use Symfony\Component\HttpKernel\Bundle\Bundle;
 6
 7/**
 8 * The CollectOnDelivery bundle class.
 9 */
10class ACMECollectOnDeliveryBundle extends Bundle
11{
12}
  1. To enable the bundle, create Resources/config/oro/bundles.yml in the same directory, with the following content:

1bundles:
2    - ACME\Bundle\CollectOnDeliveryBundle\ACMECollectOnDeliveryBundle

Hint

To fully enable a bundle, you need to regenerate the application cache. However, to save time, you can do it after creation of the payment integration.

Tip

All the files and subdirectories mentioned in the following sections of this topic are to be added to the /src/ACME/Bundle/CollectOnDeliveryBundle/ directory of your application (referred to as <bundle_root>).

Create a Payment Integration

Create an Entity to Store the Payment Method Settings

Define an entity to store the configuration settings of the payment method in the database. To do this, create <bundle_root>/Entity/CollectOnDeliverySettings.php:

  1<?php
  2
  3namespace ACME\Bundle\CollectOnDeliveryBundle\Entity;
  4
  5use Doctrine\Common\Collections\ArrayCollection;
  6use Doctrine\Common\Collections\Collection;
  7use Doctrine\ORM\Mapping as ORM;
  8use Oro\Bundle\IntegrationBundle\Entity\Transport;
  9use Oro\Bundle\LocaleBundle\Entity\LocalizedFallbackValue;
 10use Symfony\Component\HttpFoundation\ParameterBag;
 11
 12/**
 13 * Entity with settings for Collect on delivery integration
 14 *
 15 * @ORM\Entity(
 16 *     repositoryClass="ACME\Bundle\CollectOnDeliveryBundle\Entity\Repository\CollectOnDeliverySettingsRepository"
 17 * )
 18 */
 19class CollectOnDeliverySettings extends Transport
 20{
 21    /**
 22     * @var Collection|LocalizedFallbackValue[]
 23     *
 24     * @ORM\ManyToMany(
 25     *      targetEntity="Oro\Bundle\LocaleBundle\Entity\LocalizedFallbackValue",
 26     *      cascade={"ALL"},
 27     *      orphanRemoval=true
 28     * )
 29     * @ORM\JoinTable(
 30     *      name="acme_coll_on_deliv_trans_label",
 31     *      joinColumns={
 32     *          @ORM\JoinColumn(name="transport_id", referencedColumnName="id", onDelete="CASCADE")
 33     *      },
 34     *      inverseJoinColumns={
 35     *          @ORM\JoinColumn(name="localized_value_id", referencedColumnName="id", onDelete="CASCADE", unique=true)
 36     *      }
 37     * )
 38     */
 39    private $labels;
 40
 41    /**
 42     * @var Collection|LocalizedFallbackValue[]
 43     *
 44     * @ORM\ManyToMany(
 45     *      targetEntity="Oro\Bundle\LocaleBundle\Entity\LocalizedFallbackValue",
 46     *      cascade={"ALL"},
 47     *      orphanRemoval=true
 48     * )
 49     * @ORM\JoinTable(
 50     *      name="acme_coll_on_deliv_short_label",
 51     *      joinColumns={
 52     *          @ORM\JoinColumn(name="transport_id", referencedColumnName="id", onDelete="CASCADE")
 53     *      },
 54     *      inverseJoinColumns={
 55     *          @ORM\JoinColumn(name="localized_value_id", referencedColumnName="id", onDelete="CASCADE", unique=true)
 56     *      }
 57     * )
 58     */
 59    private $shortLabels;
 60
 61    /**
 62     * @var ParameterBag
 63     */
 64    private $settings;
 65
 66    public function __construct()
 67    {
 68        $this->labels = new ArrayCollection();
 69        $this->shortLabels = new ArrayCollection();
 70    }
 71
 72    /**
 73     * @return Collection|LocalizedFallbackValue[]
 74     */
 75    public function getLabels()
 76    {
 77        return $this->labels;
 78    }
 79
 80    /**
 81     * @param LocalizedFallbackValue $label
 82     *
 83     * @return $this
 84     */
 85    public function addLabel(LocalizedFallbackValue $label)
 86    {
 87        if (!$this->labels->contains($label)) {
 88            $this->labels->add($label);
 89        }
 90
 91        return $this;
 92    }
 93
 94    /**
 95     * @param LocalizedFallbackValue $label
 96     *
 97     * @return $this
 98     */
 99    public function removeLabel(LocalizedFallbackValue $label)
100    {
101        if ($this->labels->contains($label)) {
102            $this->labels->removeElement($label);
103        }
104
105        return $this;
106    }
107
108    /**
109     * @return Collection|LocalizedFallbackValue[]
110     */
111    public function getShortLabels()
112    {
113        return $this->shortLabels;
114    }
115
116    /**
117     * @param LocalizedFallbackValue $label
118     *
119     * @return $this
120     */
121    public function addShortLabel(LocalizedFallbackValue $label)
122    {
123        if (!$this->shortLabels->contains($label)) {
124            $this->shortLabels->add($label);
125        }
126
127        return $this;
128    }
129
130    /**
131     * @param LocalizedFallbackValue $label
132     *
133     * @return $this
134     */
135    public function removeShortLabel(LocalizedFallbackValue $label)
136    {
137        if ($this->shortLabels->contains($label)) {
138            $this->shortLabels->removeElement($label);
139        }
140
141        return $this;
142    }
143
144    /**
145     * @return ParameterBag
146     */
147    public function getSettingsBag()
148    {
149        if (null === $this->settings) {
150            $this->settings = new ParameterBag(
151                [
152                    'labels' => $this->getLabels(),
153                    'short_labels' => $this->getShortLabels(),
154                ]
155            );
156        }
157
158        return $this->settings;
159    }
160}

As you can see from the code above, the only two necessary parameters are defined for our collect on delivery payment method: labels and shortLabels.

Important

When naming DB columns, make sure that the name does not exceed 31 symbols. Pay attention to the acme_coll_on_deliv_short_label name in the following extract:

1     * @ORM\JoinTable(
2     *      name="acme_coll_on_deliv_short_label",
3     *      joinColumns={
4     *          @ORM\JoinColumn(name="transport_id", referencedColumnName="id", onDelete="CASCADE")
5     *      },
6     *      inverseJoinColumns={
7     *          @ORM\JoinColumn(name="localized_value_id", referencedColumnName="id", onDelete="CASCADE", unique=true)
8     *      }
9     * )

Create a Repository That Returns the Payment Method Settings

The repository returns on request the configuration settings stored by the entity that you created in the previous step. To add the repository, create <bundle_root>/Entity/Repository/CollectOnDeliverySettingsRepository.php:

 1<?php
 2
 3namespace ACME\Bundle\CollectOnDeliveryBundle\Entity\Repository;
 4
 5use ACME\Bundle\CollectOnDeliveryBundle\Entity\CollectOnDeliverySettings;
 6use Doctrine\ORM\EntityRepository;
 7
 8/**
 9 * Repository for CollectOnDeliverySettings entity
10 */
11class CollectOnDeliverySettingsRepository extends EntityRepository
12{
13    /**
14     * @return CollectOnDeliverySettings[]
15     */
16    public function getEnabledSettings()
17    {
18        return $this->createQueryBuilder('settings')
19            ->innerJoin('settings.channel', 'channel')
20            ->andWhere('channel.enabled = true')
21            ->getQuery()
22            ->getResult();
23    }
24}

Create a User Interface Form for the Payment Method Integration

When you add an integration via the user interface of the back-office, a form that contains the integration settings appears. In this step, implement the form. To do this, create <bundle_root>/Form/Type/CollectOnDeliverySettingsType.php:

 1<?php
 2
 3namespace ACME\Bundle\CollectOnDeliveryBundle\Form\Type;
 4
 5use ACME\Bundle\CollectOnDeliveryBundle\Entity\CollectOnDeliverySettings;
 6use Oro\Bundle\LocaleBundle\Form\Type\LocalizedFallbackValueCollectionType;
 7use Symfony\Component\Form\AbstractType;
 8use Symfony\Component\Form\FormBuilderInterface;
 9use Symfony\Component\OptionsResolver\OptionsResolver;
10use Symfony\Component\Validator\Constraints\NotBlank;
11
12/**
13 * Form type for Collect on delivery integration settings
14 */
15class CollectOnDeliverySettingsType extends AbstractType
16{
17    const BLOCK_PREFIX = 'acme_collect_on_delivery_setting_type';
18
19    /**
20     * {@inheritdoc}
21     */
22    public function buildForm(FormBuilderInterface $builder, array $options)
23    {
24        $builder
25            ->add(
26                'labels',
27                LocalizedFallbackValueCollectionType::class,
28                [
29                    'label' => 'acme.collect_on_delivery.settings.labels.label',
30                    'required' => true,
31                    'entry_options' => ['constraints' => [new NotBlank()]],
32                ]
33            )
34            ->add(
35                'shortLabels',
36                LocalizedFallbackValueCollectionType::class,
37                [
38                    'label' => 'acme.collect_on_delivery.settings.short_labels.label',
39                    'required' => true,
40                    'entry_options' => ['constraints' => [new NotBlank()]],
41                ]
42            );
43    }
44
45    /**
46     * {@inheritdoc}
47     */
48    public function configureOptions(OptionsResolver $resolver)
49    {
50        $resolver->setDefaults(
51            [
52                'data_class' => CollectOnDeliverySettings::class,
53            ]
54        );
55    }
56
57    /**
58     * {@inheritdoc}
59     */
60    public function getBlockPrefix()
61    {
62        return self::BLOCK_PREFIX;
63    }
64}

Create a Configuration File for the Service Container

To start using a service container for your bundle, first create the configuration file <bundle_root>/Resources/config/services.yml.

Set up Services with DependencyInjection

To set up services, load your configuration file (services.yml) using the DependencyInjection component. For this, create <bundle_root>/DependencyInjection/CollectOnDeliveryExtension.php with the following content:

 1<?php
 2
 3namespace ACME\Bundle\CollectOnDeliveryBundle\DependencyInjection;
 4
 5use Symfony\Component\Config\FileLocator;
 6use Symfony\Component\DependencyInjection\ContainerBuilder;
 7use Symfony\Component\DependencyInjection\Loader;
 8use Symfony\Component\HttpKernel\DependencyInjection\Extension;
 9
10class ACMECollectOnDeliveryExtension extends Extension
11{
12    /**
13     * {@inheritdoc}
14     */
15    public function load(array $configs, ContainerBuilder $container)
16    {
17        $configuration = new Configuration();
18        $config = $this->processConfiguration($configuration, $configs);
19
20        $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
21        $loader->load('services.yml');
22    }
23}

Add Translations for the Form Texts

To present the information on the user interface in the user-friendly way, add translations for the payment method settings’ names. To do this, create <bundle_root>/Resources/translations/messages.en.yml:

1acme:
2    collect_on_delivery:
3        settings:
4            labels.label: 'Labels'
5            short_labels.label: 'Short Labels'

Create the Integration Channel Type

When you select the type of the integration on the user interface, you will see the name and the icon that you define in this step. To implement a channel type, create <bundle_root>/Integration/CollectOnDeliveryChannelType.php:

 1<?php
 2
 3namespace ACME\Bundle\CollectOnDeliveryBundle\Integration;
 4
 5use Oro\Bundle\IntegrationBundle\Provider\ChannelInterface;
 6use Oro\Bundle\IntegrationBundle\Provider\IconAwareIntegrationInterface;
 7
 8/**
 9 * Integration channel type for Collect on delivery payment integration
10 */
11class CollectOnDeliveryChannelType implements ChannelInterface, IconAwareIntegrationInterface
12{
13    const TYPE = 'collect_on_delivery';
14
15    /**
16     * {@inheritdoc}
17     */
18    public function getLabel()
19    {
20        return 'acme.collect_on_delivery.channel_type.label';
21    }
22
23    /**
24     * {@inheritdoc}
25     */
26    public function getIcon()
27    {
28        return 'bundles/oromoneyorder/img/money-order-icon.png';
29    }
30}

Add an Icon for the Integration

To add an icon:

  1. Save the file to the <bundle_root>/Resources/public/img directory.

  2. Install assets:

    1bin/console assets:install --symlink
    

To make sure that the icon is accessible for the web interface, check if it appears (as a copy or a symlink depending on the settings selected during the application installation) in the /public/bundles/collect_on_delivery/img directory of your application.

Create the Integration Transport

A transport is generally responsible for how the data is obtained from the external system. While the Collect On Delivery method does not interact with external systems, you still need to define a transport and implement all methods of the TransportInterface for the integration to work properly. To add a transport, create <bundle_root>/Integration/CollectOnDeliveryTransport.php:

 1<?php
 2
 3namespace ACME\Bundle\CollectOnDeliveryBundle\Integration;
 4
 5use ACME\Bundle\CollectOnDeliveryBundle\Entity\CollectOnDeliverySettings;
 6use ACME\Bundle\CollectOnDeliveryBundle\Form\Type\CollectOnDeliverySettingsType;
 7use Oro\Bundle\IntegrationBundle\Entity\Transport;
 8use Oro\Bundle\IntegrationBundle\Provider\TransportInterface;
 9
10/**
11 * Transport for Collect on delivery payment integration
12 */
13class CollectOnDeliveryTransport implements TransportInterface
14{
15    /**
16     * {@inheritdoc}
17     */
18    public function init(Transport $transportEntity)
19    {
20    }
21
22    /**
23     * {@inheritdoc}
24     */
25    public function getLabel()
26    {
27        return 'acme.collect_on_delivery.settings.transport.label';
28    }
29
30    /**
31     * {@inheritdoc}
32     */
33    public function getSettingsFormType()
34    {
35        return CollectOnDeliverySettingsType::class;
36    }
37
38    /**
39     * {@inheritdoc}
40     */
41    public function getSettingsEntityFQCN()
42    {
43        return CollectOnDeliverySettings::class;
44    }
45}

Add the Channel Type and Transport to the Services Container

To register the channel type and transport, append the following key-values to <bundle_root>/Resources/config/services.yml:

 1parameters:
 2    acme_collect_on_delivery.method.identifier_prefix.collect_on_delivery: 'collect_on_delivery'
 3
 4services:
 5    acme_collect_on_delivery.generator.collect_on_delivery_config_identifier:
 6        parent: oro_integration.generator.prefixed_identifier_generator
 7        public: true
 8        arguments:
 9            - '%acme_collect_on_delivery.method.identifier_prefix.collect_on_delivery%'
10
11    acme_collect_on_delivery.integration.channel:
12        class: ACME\Bundle\CollectOnDeliveryBundle\Integration\CollectOnDeliveryChannelType
13        public: true
14        tags:
15            - { name: oro_integration.channel, type: collect_on_delivery }
16
17    acme_collect_on_delivery.integration.transport:
18        class: ACME\Bundle\CollectOnDeliveryBundle\Integration\CollectOnDeliveryTransport
19        public: false
20        tags:
21            - { name: oro_integration.transport, type: collect_on_delivery, channel_type: collect_on_delivery }

Add Translations for the Channel Type and Transport

The channel type and, in general, transport labels also appear on the user interface (you will not see the the transport label for Collect On Delivery). Provide translations for them by appending the <bundle_root>/Resources/translations/messages.en.yml. Now, the messages.en.yml content must look as follows:

1acme:
2    collect_on_delivery:
3        settings:
4            labels.label: 'Labels'
5            short_labels.label: 'Short Labels'
6            transport.label: 'Collect on delivery'
7
8        channel_type.label: 'Collect on delivery'
9        payment_method_message: 'Pay on delivery'

Add an Installer

An installer ensures that upon the application installation, the database will contain the entity that you defined within your bundle.

Follow the instructions provided in the How to generate an installer topic to apply the changes without migration and generate an installer file based on the current schema of the DB.

Note

If you have not performed the steps mentioned in How to generate an installer, because you already have the installer file, then make sure to run the php bin/console oro:migration:load --force command to apply the changes from the file.

After you complete it, you will have the class <bundle_root>/Migrations/Schema/CollectOnDeliveryBundleInstaller.php with the following content:

  1<?php
  2
  3namespace ACME\Bundle\CollectOnDeliveryBundle\Migrations\Schema;
  4
  5use Doctrine\DBAL\Schema\Schema;
  6use Oro\Bundle\MigrationBundle\Migration\Installation;
  7use Oro\Bundle\MigrationBundle\Migration\QueryBag;
  8
  9/**
 10 * @SuppressWarnings(PHPMD.TooManyMethods)
 11 * @SuppressWarnings(PHPMD.ExcessiveClassLength)
 12 */
 13class ACMECollectOnDeliveryBundleInstaller implements Installation
 14{
 15    /**
 16     * {@inheritdoc}
 17     */
 18    public function getMigrationVersion()
 19    {
 20        return 'v1_0';
 21    }
 22
 23    /**
 24     * {@inheritdoc}
 25     */
 26    public function up(Schema $schema, QueryBag $queries)
 27    {
 28        /** Tables generation **/
 29        $this->createAcmeCollOnDelivTransLabelTable($schema);
 30        $this->createAcmeCollOnDelivShortLabelTable($schema);
 31
 32        /** Foreign keys generation **/
 33        $this->addAcmeCollOnDelivTransLabelForeignKeys($schema);
 34        $this->addAcmeCollOnDelivShortLabelForeignKeys($schema);
 35    }
 36
 37    /**
 38     * Create acme_coll_on_deliv_trans_label table
 39     *
 40     * @param Schema $schema
 41     */
 42    protected function createAcmeCollOnDelivTransLabelTable(Schema $schema)
 43    {
 44        $table = $schema->createTable('acme_coll_on_deliv_trans_label');
 45        $table->addColumn('transport_id', 'integer', []);
 46        $table->addColumn('localized_value_id', 'integer', []);
 47        $table->setPrimaryKey(['transport_id', 'localized_value_id']);
 48        $table->addIndex(['transport_id'], 'idx_13476d069909c13f', []);
 49        $table->addUniqueIndex(['localized_value_id'], 'uniq_13476d06eb576e89');
 50    }
 51
 52    /**
 53     * Create acme_coll_on_deliv_short_label table
 54     *
 55     * @param Schema $schema
 56     */
 57    protected function createAcmeCollOnDelivShortLabelTable(Schema $schema)
 58    {
 59        $table = $schema->createTable('acme_coll_on_deliv_short_label');
 60        $table->addColumn('transport_id', 'integer', []);
 61        $table->addColumn('localized_value_id', 'integer', []);
 62        $table->addUniqueIndex(['localized_value_id'], 'uniq_2c81a8dceb576e89');
 63        $table->addIndex(['transport_id'], 'idx_2c81a8dc9909c13f', []);
 64        $table->setPrimaryKey(['transport_id', 'localized_value_id']);
 65    }
 66
 67    /**
 68     * Add acme_coll_on_deliv_trans_label foreign keys.
 69     *
 70     * @param Schema $schema
 71     */
 72    protected function addAcmeCollOnDelivTransLabelForeignKeys(Schema $schema)
 73    {
 74        $table = $schema->getTable('acme_coll_on_deliv_trans_label');
 75        $table->addForeignKeyConstraint(
 76            $schema->getTable('oro_fallback_localization_val'),
 77            ['localized_value_id'],
 78            ['id'],
 79            ['onUpdate' => null, 'onDelete' => 'CASCADE']
 80        );
 81        $table->addForeignKeyConstraint(
 82            $schema->getTable('oro_integration_transport'),
 83            ['transport_id'],
 84            ['id'],
 85            ['onUpdate' => null, 'onDelete' => 'CASCADE']
 86        );
 87    }
 88
 89    /**
 90     * Add acme_coll_on_deliv_short_label foreign keys.
 91     *
 92     * @param Schema $schema
 93     */
 94    protected function addAcmeCollOnDelivShortLabelForeignKeys(Schema $schema)
 95    {
 96        $table = $schema->getTable('acme_coll_on_deliv_short_label');
 97        $table->addForeignKeyConstraint(
 98            $schema->getTable('oro_fallback_localization_val'),
 99            ['localized_value_id'],
100            ['id'],
101            ['onUpdate' => null, 'onDelete' => 'CASCADE']
102        );
103        $table->addForeignKeyConstraint(
104            $schema->getTable('oro_integration_transport'),
105            ['transport_id'],
106            ['id'],
107            ['onUpdate' => null, 'onDelete' => 'CASCADE']
108        );
109    }
110}

Check That the Integration is Created Successfully

  1. Clear the application cache:

    1bin/console cache:clear
    

    Note

    If you are working in production environment, you have to use the --env=prod parameter with the command.

  2. Open the user interface and check that the changes have applied and you can add an integration of the Collect On Delivery type.

Implement a Payment Method

Now implement the payment method itself.

Create a Factory for the Payment Method Configuration

A configuration factory generates an individual configuration set for each instance of the integration of the Collect On Delivery type.

To add a payment method configuration factory, in the directory <bundle_root>/PaymentMethod/Config/Factory/ create interface CollectOnDeliveryConfigFactoryInterface.php and the class CollectOnDeliveryConfigFactory.php that implements this interface:

Configuration Factory Interface

 1<?php
 2
 3namespace ACME\Bundle\CollectOnDeliveryBundle\PaymentMethod\Config\Factory;
 4
 5use ACME\Bundle\CollectOnDeliveryBundle\Entity\CollectOnDeliverySettings;
 6use ACME\Bundle\CollectOnDeliveryBundle\PaymentMethod\Config\CollectOnDeliveryConfigInterface;
 7
 8/**
 9 * Interface for Collect on delivery payment method config factory
10 * Creates instances of CollectOnDeliverySettings with configuration for payment method
11 */
12interface CollectOnDeliveryConfigFactoryInterface
13{
14    /**
15     * @param CollectOnDeliverySettings $settings
16     * @return CollectOnDeliveryConfigInterface
17     */
18    public function create(CollectOnDeliverySettings $settings);
19}

Configuration Factory Class

 1<?php
 2
 3namespace ACME\Bundle\CollectOnDeliveryBundle\PaymentMethod\Config\Factory;
 4
 5use ACME\Bundle\CollectOnDeliveryBundle\Entity\CollectOnDeliverySettings;
 6use ACME\Bundle\CollectOnDeliveryBundle\PaymentMethod\Config\CollectOnDeliveryConfig;
 7use Doctrine\Common\Collections\Collection;
 8use Oro\Bundle\IntegrationBundle\Generator\IntegrationIdentifierGeneratorInterface;
 9use Oro\Bundle\LocaleBundle\Helper\LocalizationHelper;
10
11/**
12 * Creates instances of configurations for Collect on delivery payment method
13 */
14class CollectOnDeliveryConfigFactory implements CollectOnDeliveryConfigFactoryInterface
15{
16    /**
17     * @var LocalizationHelper
18     */
19    private $localizationHelper;
20
21    /**
22     * @var IntegrationIdentifierGeneratorInterface
23     */
24    private $identifierGenerator;
25
26    /**
27     * @param LocalizationHelper $localizationHelper
28     * @param IntegrationIdentifierGeneratorInterface $identifierGenerator
29     */
30    public function __construct(
31        LocalizationHelper $localizationHelper,
32        IntegrationIdentifierGeneratorInterface $identifierGenerator
33    ) {
34        $this->localizationHelper = $localizationHelper;
35        $this->identifierGenerator = $identifierGenerator;
36    }
37
38    /**
39     * {@inheritDoc}
40     */
41    public function create(CollectOnDeliverySettings $settings)
42    {
43        $params = [];
44        $channel = $settings->getChannel();
45
46        $params[CollectOnDeliveryConfig::FIELD_LABEL] = $this->getLocalizedValue($settings->getLabels());
47        $params[CollectOnDeliveryConfig::FIELD_SHORT_LABEL] = $this->getLocalizedValue($settings->getShortLabels());
48        $params[CollectOnDeliveryConfig::FIELD_ADMIN_LABEL] = $channel->getName();
49        $params[CollectOnDeliveryConfig::FIELD_PAYMENT_METHOD_IDENTIFIER] =
50            $this->identifierGenerator->generateIdentifier($channel);
51
52        return new CollectOnDeliveryConfig($params);
53    }
54
55    /**
56     * @param Collection $values
57     *
58     * @return string
59     */
60    private function getLocalizedValue(Collection $values)
61    {
62        return (string)$this->localizationHelper->getLocalizedValue($values);
63    }
64}

Create a Provider for the Payment Method Configuration

A configuration provider accepts and integration id and returns settings based on it.

To add a payment method configuration provider, in the directory <bundle_root>/PaymentMethod/Config/Provider/ create interface CollectOnDeliveryConfigProviderInterface.php and the class CollectOnDeliveryConfigProvider.php that implements this interface:

Configuration Provider Interface

 1<?php
 2
 3namespace ACME\Bundle\CollectOnDeliveryBundle\PaymentMethod\Config\Provider;
 4
 5use ACME\Bundle\CollectOnDeliveryBundle\PaymentMethod\Config\CollectOnDeliveryConfigInterface;
 6
 7/**
 8 * Interface for config provider which allows to get configs based on payment method identifier
 9 */
10interface CollectOnDeliveryConfigProviderInterface
11{
12    /**
13     * @return CollectOnDeliveryConfigInterface[]
14     */
15    public function getPaymentConfigs();
16
17    /**
18     * @param string $identifier
19     * @return CollectOnDeliveryConfigInterface|null
20     */
21    public function getPaymentConfig($identifier);
22
23    /**
24     * @param string $identifier
25     * @return bool
26     */
27    public function hasPaymentConfig($identifier);
28}

Configuration Provider Class

  1<?php
  2
  3namespace ACME\Bundle\CollectOnDeliveryBundle\PaymentMethod\Config\Provider;
  4
  5use ACME\Bundle\CollectOnDeliveryBundle\Entity\CollectOnDeliverySettings;
  6use ACME\Bundle\CollectOnDeliveryBundle\Entity\Repository\CollectOnDeliverySettingsRepository;
  7use ACME\Bundle\CollectOnDeliveryBundle\PaymentMethod\Config\CollectOnDeliveryConfigInterface;
  8use ACME\Bundle\CollectOnDeliveryBundle\PaymentMethod\Config\Factory\CollectOnDeliveryConfigFactoryInterface;
  9use Doctrine\Common\Persistence\ManagerRegistry;
 10use Psr\Log\LoggerInterface;
 11
 12/**
 13 * Allows to get configs of Collect on delivery payment method
 14 */
 15class CollectOnDeliveryConfigProvider implements CollectOnDeliveryConfigProviderInterface
 16{
 17    /**
 18     * @var ManagerRegistry
 19     */
 20    protected $doctrine;
 21
 22    /**
 23     * @var CollectOnDeliveryConfigFactoryInterface
 24     */
 25    protected $configFactory;
 26
 27    /**
 28     * @var CollectOnDeliveryConfigInterface[]
 29     */
 30    protected $configs;
 31
 32    /**
 33     * @var LoggerInterface
 34     */
 35    protected $logger;
 36
 37    /**
 38     * @param ManagerRegistry $doctrine
 39     * @param LoggerInterface $logger
 40     * @param CollectOnDeliveryConfigFactoryInterface $configFactory
 41     */
 42    public function __construct(
 43        ManagerRegistry $doctrine,
 44        LoggerInterface $logger,
 45        CollectOnDeliveryConfigFactoryInterface $configFactory
 46    ) {
 47        $this->doctrine = $doctrine;
 48        $this->logger = $logger;
 49        $this->configFactory = $configFactory;
 50    }
 51
 52    /**
 53     * {@inheritDoc}
 54     */
 55    public function getPaymentConfigs()
 56    {
 57        $configs = [];
 58
 59        $settings = $this->getEnabledIntegrationSettings();
 60
 61        foreach ($settings as $setting) {
 62            $config = $this->configFactory->create($setting);
 63
 64            $configs[$config->getPaymentMethodIdentifier()] = $config;
 65        }
 66
 67        return $configs;
 68    }
 69
 70    /**
 71     * {@inheritDoc}
 72     */
 73    public function getPaymentConfig($identifier)
 74    {
 75        $paymentConfigs = $this->getPaymentConfigs();
 76
 77        if ([] === $paymentConfigs || false === array_key_exists($identifier, $paymentConfigs)) {
 78            return null;
 79        }
 80
 81        return $paymentConfigs[$identifier];
 82    }
 83
 84    /**
 85     * {@inheritDoc}
 86     */
 87    public function hasPaymentConfig($identifier)
 88    {
 89        return null !== $this->getPaymentConfig($identifier);
 90    }
 91
 92    /**
 93     * @return CollectOnDeliverySettings[]
 94     */
 95    protected function getEnabledIntegrationSettings()
 96    {
 97        try {
 98            /** @var CollectOnDeliverySettingsRepository $repository */
 99            $repository = $this->doctrine
100                ->getManagerForClass(CollectOnDeliverySettings::class)
101                ->getRepository(CollectOnDeliverySettings::class);
102
103            return $repository->getEnabledSettings();
104        } catch (\UnexpectedValueException $e) {
105            $this->logger->critical($e->getMessage());
106
107            return [];
108        }
109    }
110}

Implement Payment Method Configuration

In the <bundle_root>/PaymentMethod/Config directory, create the CollectOnDeliveryConfigInterface.php interface and the CollectOnDeliveryConfig.php class that implements this interface:

Configuration Interface

 1<?php
 2
 3namespace ACME\Bundle\CollectOnDeliveryBundle\PaymentMethod\Config;
 4
 5use Oro\Bundle\PaymentBundle\Method\Config\PaymentConfigInterface;
 6
 7/**
 8 * Interface that describes specific configuration for Collect on delivery payment method
 9 */
10interface CollectOnDeliveryConfigInterface extends PaymentConfigInterface
11{
12}

Configuration Class

 1<?php
 2
 3namespace ACME\Bundle\CollectOnDeliveryBundle\PaymentMethod\Config;
 4
 5use Oro\Bundle\PaymentBundle\Method\Config\ParameterBag\AbstractParameterBagPaymentConfig;
 6
 7/**
 8 * Configuration class which is used to get specific configuration for Collect on delivery payment method
 9 * Usually it has additional get methods for payment type specific configurations
10 */
11class CollectOnDeliveryConfig extends AbstractParameterBagPaymentConfig implements CollectOnDeliveryConfigInterface
12{
13}

Add the Payment Method Configuration Factory and Provider to the Services Container

To register the payment method configuration factory and provider, append the following key-values to <bundle_root>/Resources/config/services.yml:

 1    acme_collect_on_delivery.factory.collect_on_delivery_config:
 2        class: ACME\Bundle\CollectOnDeliveryBundle\PaymentMethod\Config\Factory\CollectOnDeliveryConfigFactory
 3        public: false
 4        arguments:
 5            - '@oro_locale.helper.localization'
 6            - '@acme_collect_on_delivery.generator.collect_on_delivery_config_identifier'
 7
 8    acme_collect_on_delivery.payment_method.config.provider:
 9        class: ACME\Bundle\CollectOnDeliveryBundle\PaymentMethod\Config\Provider\CollectOnDeliveryConfigProvider
10        arguments:
11            - '@doctrine'
12            - '@logger'
13            - '@acme_collect_on_delivery.factory.collect_on_delivery_config'

Create a Factory for the Payment Method View

Views provide the set of options for the payment method blocks that users see when they select the Collect on Delivery payment method and review the orders during the checkout.

To add a payment method view factory, in the directory <bundle_root>/PaymentMethod/View/Factory/ create interface CollectOnDeliveryViewFactoryInterface.php and the class CollectOnDeliveryViewFactory.php that implements this interface:

Payment Method View Factory Interface

 1<?php
 2
 3namespace ACME\Bundle\CollectOnDeliveryBundle\PaymentMethod\View\Factory;
 4
 5use ACME\Bundle\CollectOnDeliveryBundle\PaymentMethod\Config\CollectOnDeliveryConfigInterface;
 6use Oro\Bundle\PaymentBundle\Method\View\PaymentMethodViewInterface;
 7
 8/**
 9 * Factory for creating views of Collect on delivery payment method
10 */
11interface CollectOnDeliveryViewFactoryInterface
12{
13    /**
14     * @param CollectOnDeliveryConfigInterface $config
15     * @return PaymentMethodViewInterface
16     */
17    public function create(CollectOnDeliveryConfigInterface $config);
18}

Payment Method View Factory Class

 1<?php
 2
 3namespace ACME\Bundle\CollectOnDeliveryBundle\PaymentMethod\View\Factory;
 4
 5use ACME\Bundle\CollectOnDeliveryBundle\PaymentMethod\Config\CollectOnDeliveryConfigInterface;
 6use ACME\Bundle\CollectOnDeliveryBundle\PaymentMethod\View\CollectOnDeliveryView;
 7
 8/**
 9 * Factory for creating views of Collect on delivery payment method
10 */
11class CollectOnDeliveryViewFactory implements CollectOnDeliveryViewFactoryInterface
12{
13    /**
14     * {@inheritdoc}
15     */
16    public function create(CollectOnDeliveryConfigInterface $config)
17    {
18        return new CollectOnDeliveryView($config);
19    }
20}

Create Provider for the Payment Method View

To add a payment method view provider, create <bundle_root>/PaymentMethod/View/Provider/CollectOnDeliveryViewProvider.php:

Payment Method View Provider Class

 1<?php
 2
 3namespace ACME\Bundle\CollectOnDeliveryBundle\PaymentMethod\View\Provider;
 4
 5use ACME\Bundle\CollectOnDeliveryBundle\PaymentMethod\Config\CollectOnDeliveryConfigInterface;
 6use ACME\Bundle\CollectOnDeliveryBundle\PaymentMethod\Config\Provider\CollectOnDeliveryConfigProviderInterface;
 7use ACME\Bundle\CollectOnDeliveryBundle\PaymentMethod\View\Factory\CollectOnDeliveryViewFactoryInterface;
 8use Oro\Bundle\PaymentBundle\Method\View\AbstractPaymentMethodViewProvider;
 9
10/**
11 * Provider for retrieving payment method view instances
12 */
13class CollectOnDeliveryViewProvider extends AbstractPaymentMethodViewProvider
14{
15    /** @var CollectOnDeliveryViewFactoryInterface */
16    private $factory;
17
18    /** @var CollectOnDeliveryConfigProviderInterface */
19    private $configProvider;
20
21    /**
22     * @param CollectOnDeliveryConfigProviderInterface $configProvider
23     * @param CollectOnDeliveryViewFactoryInterface $factory
24     */
25    public function __construct(
26        CollectOnDeliveryConfigProviderInterface $configProvider,
27        CollectOnDeliveryViewFactoryInterface $factory
28    ) {
29        $this->factory = $factory;
30        $this->configProvider = $configProvider;
31
32        parent::__construct();
33    }
34
35    /**
36     * {@inheritdoc}
37     */
38    protected function buildViews()
39    {
40        $configs = $this->configProvider->getPaymentConfigs();
41        foreach ($configs as $config) {
42            $this->addCollectOnDeliveryView($config);
43        }
44    }
45
46    /**
47     * @param CollectOnDeliveryConfigInterface $config
48     */
49    protected function addCollectOnDeliveryView(CollectOnDeliveryConfigInterface $config)
50    {
51        $this->addView(
52            $config->getPaymentMethodIdentifier(),
53            $this->factory->create($config)
54        );
55    }
56}

Implement the Payment Method View

Finally, to implement the payment method view, create <bundle_root>/PaymentMethod/ViewCollectOnDeliveryView.php:

Payment Method View Class

 1<?php
 2
 3namespace ACME\Bundle\CollectOnDeliveryBundle\PaymentMethod\View;
 4
 5use ACME\Bundle\CollectOnDeliveryBundle\PaymentMethod\Config\CollectOnDeliveryConfigInterface;
 6use Oro\Bundle\PaymentBundle\Context\PaymentContextInterface;
 7use Oro\Bundle\PaymentBundle\Method\View\PaymentMethodViewInterface;
 8
 9/**
10 * View for Collect on delivery payment method
11 */
12class CollectOnDeliveryView implements PaymentMethodViewInterface
13{
14    /**
15     * @var CollectOnDeliveryConfigInterface
16     */
17    protected $config;
18
19    /**
20     * @param CollectOnDeliveryConfigInterface $config
21     */
22    public function __construct(CollectOnDeliveryConfigInterface $config)
23    {
24        $this->config = $config;
25    }
26
27    /**
28     * {@inheritdoc}
29     */
30    public function getOptions(PaymentContextInterface $context)
31    {
32        return [];
33    }
34
35    /**
36     * {@inheritdoc}
37     */
38    public function getBlock()
39    {
40        return '_payment_methods_collect_on_delivery_widget';
41    }
42
43    /**
44     * {@inheritdoc}
45     */
46    public function getLabel()
47    {
48        return $this->config->getLabel();
49    }
50
51    /**
52     * {@inheritdoc}
53     */
54    public function getShortLabel()
55    {
56        return $this->config->getShortLabel();
57    }
58
59    /**
60     * {@inheritdoc}
61     */
62    public function getAdminLabel()
63    {
64        return $this->config->getAdminLabel();
65    }
66
67    /** {@inheritdoc} */
68    public function getPaymentMethodIdentifier()
69    {
70        return $this->config->getPaymentMethodIdentifier();
71    }
72}

Add the Payment Method View Factory and Provider to the Services Container

To register the payment method view factory and provider, append the following key-values to <bundle_root>/Resources/config/services.yml:

 1    acme_collect_on_delivery.payment_method_view_provider.collect_on_delivery:
 2        class: ACME\Bundle\CollectOnDeliveryBundle\PaymentMethod\View\Provider\CollectOnDeliveryViewProvider
 3        public: false
 4        arguments:
 5            - '@acme_collect_on_delivery.payment_method.config.provider'
 6            - '@acme_collect_on_delivery.factory.method_view.collect_on_delivery'
 7        tags:
 8            - { name: oro_payment.payment_method_view_provider }
 9
10    acme_collect_on_delivery.factory.method.collect_on_delivery:
11        class: ACME\Bundle\CollectOnDeliveryBundle\PaymentMethod\Factory\CollectOnDeliveryPaymentMethodFactory
12        public: false

Create a Factory for the Main Method

To add a payment method factory, in the directory <bundle_root>/PaymentMethod/Factory/ create interface CollectOnDeliveryPaymentMethodFactoryInterface.php and the class CollectOnDeliveryPaymentMethodFactory.php that implements this interface:

Factory Interface

 1<?php
 2
 3namespace ACME\Bundle\CollectOnDeliveryBundle\PaymentMethod\Factory;
 4
 5use ACME\Bundle\CollectOnDeliveryBundle\PaymentMethod\Config\CollectOnDeliveryConfigInterface;
 6use Oro\Bundle\PaymentBundle\Method\PaymentMethodInterface;
 7
 8/**
 9 * Interface of factories which create payment method instances based on configuration
10 */
11interface CollectOnDeliveryPaymentMethodFactoryInterface
12{
13    /**
14     * @param CollectOnDeliveryConfigInterface $config
15     * @return PaymentMethodInterface
16     */
17    public function create(CollectOnDeliveryConfigInterface $config);
18}

Factory Class

 1<?php
 2
 3namespace ACME\Bundle\CollectOnDeliveryBundle\PaymentMethod\Factory;
 4
 5use ACME\Bundle\CollectOnDeliveryBundle\PaymentMethod\CollectOnDelivery;
 6use ACME\Bundle\CollectOnDeliveryBundle\PaymentMethod\Config\CollectOnDeliveryConfigInterface;
 7
 8/**
 9 * Factory creates payment method instances based on configuration
10 */
11class CollectOnDeliveryPaymentMethodFactory implements CollectOnDeliveryPaymentMethodFactoryInterface
12{
13    /**
14     * {@inheritdoc}
15     */
16    public function create(CollectOnDeliveryConfigInterface $config)
17    {
18        return new CollectOnDelivery($config);
19    }
20}

Create Provider for the Main Method

To add a payment method provider, create <bundle_root>/PaymentMethod/Provider/CollectOnDeliveryProvider.php:

Provider Class

 1<?php
 2
 3namespace ACME\Bundle\CollectOnDeliveryBundle\PaymentMethod\Provider;
 4
 5use ACME\Bundle\CollectOnDeliveryBundle\PaymentMethod\Config\CollectOnDeliveryConfigInterface;
 6use ACME\Bundle\CollectOnDeliveryBundle\PaymentMethod\Config\Provider\CollectOnDeliveryConfigProviderInterface;
 7use ACME\Bundle\CollectOnDeliveryBundle\PaymentMethod\Factory\CollectOnDeliveryPaymentMethodFactoryInterface;
 8use Oro\Bundle\PaymentBundle\Method\Provider\AbstractPaymentMethodProvider;
 9
10/**
11 * Provider for retrieving configured payment method instances
12 */
13class CollectOnDeliveryMethodProvider extends AbstractPaymentMethodProvider
14{
15    /**
16     * @var CollectOnDeliveryPaymentMethodFactoryInterface
17     */
18    protected $factory;
19
20    /**
21     * @var CollectOnDeliveryConfigProviderInterface
22     */
23    private $configProvider;
24
25    /**
26     * @param CollectOnDeliveryConfigProviderInterface $configProvider
27     * @param CollectOnDeliveryPaymentMethodFactoryInterface $factory
28     */
29    public function __construct(
30        CollectOnDeliveryConfigProviderInterface $configProvider,
31        CollectOnDeliveryPaymentMethodFactoryInterface $factory
32    ) {
33        parent::__construct();
34
35        $this->configProvider = $configProvider;
36        $this->factory = $factory;
37    }
38
39    /**
40     * {@inheritdoc}
41     */
42    protected function collectMethods()
43    {
44        $configs = $this->configProvider->getPaymentConfigs();
45        foreach ($configs as $config) {
46            $this->addCollectOnDeliveryMethod($config);
47        }
48    }
49
50    /**
51     * @param CollectOnDeliveryConfigInterface $config
52     */
53    protected function addCollectOnDeliveryMethod(CollectOnDeliveryConfigInterface $config)
54    {
55        $this->addMethod(
56            $config->getPaymentMethodIdentifier(),
57            $this->factory->create($config)
58        );
59    }
60}

Implement the Main Method

Now, implement the main method. To do this, create the <bundle_root>/PaymentMethod/CollectOnDelivery.php class:

Class

 1<?php
 2
 3namespace ACME\Bundle\CollectOnDeliveryBundle\PaymentMethod;
 4
 5use ACME\Bundle\CollectOnDeliveryBundle\PaymentMethod\Config\CollectOnDeliveryConfigInterface;
 6use Oro\Bundle\PaymentBundle\Context\PaymentContextInterface;
 7use Oro\Bundle\PaymentBundle\Entity\PaymentTransaction;
 8use Oro\Bundle\PaymentBundle\Method\PaymentMethodInterface;
 9
10/**
11 * Payment method class that describes main business logic of Collect on delivery payment method
12 * It creates invoice payment transaction
13 */
14class CollectOnDelivery implements PaymentMethodInterface
15{
16    /**
17     * @var CollectOnDeliveryConfigInterface
18     */
19    private $config;
20
21    /**
22     * @param CollectOnDeliveryConfigInterface $config
23     */
24    public function __construct(CollectOnDeliveryConfigInterface $config)
25    {
26        $this->config = $config;
27    }
28
29    /**
30     * {@inheritdoc}
31     */
32    public function execute($action, PaymentTransaction $paymentTransaction)
33    {
34        $paymentTransaction->setAction(PaymentMethodInterface::INVOICE);
35        $paymentTransaction->setActive(true);
36        $paymentTransaction->setSuccessful(true);
37
38        return [];
39    }
40
41    /**
42     * {@inheritdoc}
43     */
44    public function getIdentifier()
45    {
46        return $this->config->getPaymentMethodIdentifier();
47    }
48
49    /**
50     * {@inheritdoc}
51     */
52    public function isApplicable(PaymentContextInterface $context)
53    {
54        return true;
55    }
56
57    /**
58     * {@inheritdoc}
59     */
60    public function supports($actionName)
61    {
62        return $actionName === self::PURCHASE;
63    }
64}

Hint

Pay attention to the lines:

1    public function supports($actionName)
2    {
3        return $actionName === self::PURCHASE;
4    }

This is where you define which transaction types are associated with the payment method. To keep it simple, for Collect On Delivery a single transaction is defined. Thus, it will work the following way: when a user submits an order, the “purchase” transaction takes place, and the order status becomes “purchased”.

Check PaymentMethodInterface for more information on other predefined transactions.

Add the Payment Method Factory and Provider to the Services Container

To register the payment method main factory and provider, append the following key-values to <bundle_root>/Resources/config/services.yml:

 1    acme_collect_on_delivery.factory.method.collect_on_delivery:
 2        class: ACME\Bundle\CollectOnDeliveryBundle\PaymentMethod\Factory\CollectOnDeliveryPaymentMethodFactory
 3        public: false
 4
 5    acme_collect_on_delivery.payment_method_provider.collect_on_delivery:
 6        class: ACME\Bundle\CollectOnDeliveryBundle\PaymentMethod\Provider\CollectOnDeliveryMethodProvider
 7        public: false
 8        arguments:
 9            - '@acme_collect_on_delivery.payment_method.config.provider'
10            - '@acme_collect_on_delivery.factory.method.collect_on_delivery'
11        tags:
12            - { name: oro_payment.payment_method_provider }

Define the Payment Method’s Layouts for the Storefront

Layouts provide the html template for the payment method blocks that users see when doing the checkout in the storefront. There are two different blocks: one that users see during selection of the payment method, and the other that they see when reviewing the order. You need to define templates for each of these blocks.

For this, in the directory <bundle_root>/Resources/views/layouts/default/imports/, create templates for the payment method selection checkout step:

  • oro_payment_method_options/layout.html.twig

  • oro_payment_method_options/layout.html

and for the order review:

  • oro_payment_method_order_submit/layout.html.twig

  • oro_payment_method_order_submit/layout.html

layout.html.twig for the Payment Method Selection

1{% block _payment_methods_collect_on_delivery_widget %}
2    <div class="{{ class_prefix }}-form__payment-methods">
3        <table class="grid">
4            <tr>
5                <td>{{ 'acme.collect_on_delivery.payment_method_message'|trans }}</td>
6            </tr>
7        </table>
8    </div>
9{% endblock %}

Note that the custom message to appear in the block is defined. Do not forget to add translations in the messages.en.yml for any custom text that you add.

layout.html for the Payment Method Selection

1layout:
2    actions:
3        - '@setBlockTheme':
4            themes:
5                - 'layout.html.twig'

layout.html.twig for the Order Review

1{% block _order_review_payment_methods_collect_on_delivery_widget -%}
2    {% if options.payment_method is defined %}
3        <div class="hidden"
4             data-page-component-module="oropayment/js/app/components/payment-method-component"
5             data-page-component-options="{{ {paymentMethod: options.payment_method}|json_encode }}">
6        </div>
7    {% endif %}
8{%- endblock %}

layout.html for the Order Review

1layout:
2    actions:
3        - '@setBlockTheme':
4            themes:
5                - 'layout.html.twig'

Define a Translation for the Custom Message

In step, you have added a custom message to the payment method block. Define a translation for it in the messages.en.yml which now should look like the following:

1acme:
2    collect_on_delivery:
3        settings:
4            labels.label: 'Labels'
5            short_labels.label: 'Short Labels'
6            transport.label: 'Collect on delivery'
7
8        channel_type.label: 'Collect on delivery'
9        payment_method_message: 'Pay on delivery'

Check That Payment Method is Added

Now, the Collect On Delivery payment method is fully implemented.

Clear the application cache, open the user interface and try to submit an order.