Skip over navigation

Join Stripe & Oro, As We Share Scalable Payment Integration Patterns for Enterprise eCommerce on March 6, 9 am CST/4 pm CET!

Book Your Spot
HomeBlogExtending payment methods in OroCommerce

Developers' Digest

Extending payment methods in OroCommerce

February 20, 2018 | msarandi

blog-hero

In the rapidly changing eCommerce world, multiple payment options put every buyer just a few clicks from the purchase. Out of the box, OroCommerce supports the most popular and trusted payment methods:

  • PayPal Payments Pro.
  • Payflow gateway.
  • Invoices with configurable payment terms.
  • Check/Money Order.

You can hardly predict the next most popular online and offline payment company in your customer’s area or the next high-tech payment method to emerge, but you can be prepared to the change. With flexible and customizable open-source OroCommerce application, you can adjust quickly and support it the next day (well, maybe the next week).

This topic will guide you through adding a custom payment method for your OroCommerce-based B2B store. For this example. we’ve chosen collect-on-delivery payment, as it comes in many flavors (cash, credit card, and online wallet) and many people still consider it the most credible and risk-free option for online purchases.

Adding a new payment method

Step 1. Create and enable a bundle

Note: See Symfony best practices for naming and placing your bundle.

Create a new bundle that extends the PaymentBundle and enable it using the steps explained here. Briefly, they are:

  • Run a Symfony generate:bundle: command.
    Note: Kernel and routing updates are not necessary, as OroPlatform automated these steps.
  • Create a bundles.yml file in Resources/config/oro/ folder with the following content:
        bundles:
            { name: OroBundleCollectOnDeliveryCollectOnDeliveryBundle, 
              priority: 100 }
  • Regenerate application cache using the cache:clear: command.
    Note: In a production environment, add –env=prod parameter to the command.

Step 2. Implement payment method configuration

To define custom configurable options and manage visibility and availability of the payment method, bind it to the Configuration Settings using the following steps:

  • Update Configuration.php class in OroBundleCollectOnDeliveryDependencyInjection namespace with the configuration options, like in this example:
    class Configuration implements ConfigurationInterface
        {
            const COLLECT_ON_DELIVERY_ENABLED_KEY = 'collect_on_delivery_enabled';
            const COLLECT_ON_DELIVERY_LABEL_KEY = 'collect_on_delivery_label';
            const COLLECT_ON_DELIVERY_SHORT_LABEL_KEY = 'collect_on_delivery_short_label';
            const COLLECT_ON_DELIVERY_SORT_ORDER_KEY = 'collect_on_delivery_sort_order';
            const COLLECT_ON_DELIVERY_ALLOWED_COUNTRIES_KEY = 'collect_on_delivery_allowed_countries';
            const COLLECT_ON_DELIVERY_SELECTED_COUNTRIES_KEY = 'collect_on_delivery_selected_countries';
            const COLLECT_ON_DELIVERY_ALLOWED_CURRENCIES = 'collect_on_delivery_allowed_currencies';
            /**
             * {@inheritdoc}
             */
            public function getConfigTreeBuilder()
            {
                $treeBuilder = new TreeBuilder();
                $rootNode = $treeBuilder->root('collect_on_delivery');
                SettingsBuilder::append(
                    $rootNode,
                    [
                        self::COLLECT_ON_DELIVERY_ENABLED_KEY => [
                            'type' => 'boolean',
                            'value' => false,
                        ],
                        self::COLLECT_ON_DELIVERY_LABEL_KEY => [
                            'type' => 'text',
                            'value' => 'Collect On Delivery',
                        ],
                        self::COLLECT_ON_DELIVERY_SHORT_LABEL_KEY => [
                            'type' => 'text',
                            'value' => 'Collect On Delivery',
                        ],
                        self::COLLECT_ON_DELIVERY_SORT_ORDER_KEY => [
                            'type' => 'string',
                            'value' => 60,
                        ],
                        self::COLLECT_ON_DELIVERY_ALLOWED_COUNTRIES_KEY => [
                            'type' => 'text',
                            'value' => PaymentConfiguration::ALLOWED_COUNTRIES_ALL,
                        ],
                        self::COLLECT_ON_DELIVERY_SELECTED_COUNTRIES_KEY => [
                            'type' => 'array',
                            'value' => [],
                        ],
                        self::COLLECT_ON_DELIVERY_ALLOWED_CURRENCIES => [
                            'type' => 'array',
                            'value' => CurrencyConfiguration::$defaultCurrencies,
                        ],
                    ]
                );
                return $treeBuilder;
            }
        }
  • Next, define the custom configuration options in the system_configuration.yml in the Oro/Bundle/CollectOnDeliveryBundle/Resources/config/ folder. See the system configuration sample:
        oro_system_configuration:
        groups:
            collect_on_delivery:
                title: oro.collect_on_delivery.system_configuration.groups.collect_on_delivery.title
            collect_on_delivery_display:
                title: oro.collect_on_delivery.system_configuration.groups.display_options.title
        fields:
            collect_on_delivery.collect_on_delivery_enabled:
                data_type: boolean
                type: oro_config_checkbox
                priority: 100
                options:
                    label: oro.collect_on_delivery.system_configuration.fields.enabled.label
    
            collect_on_delivery.collect_on_delivery_label:
                data_type: string
                type: text
                priority: 90
                options:
                    label: orob2b.payment.system_configuration.fields.label.label
                    tooltip: orob2b.payment.system_configuration.fields.label.tooltip
                    constraints:
                        - NotBlank: ~
    
            collect_on_delivery.collect_on_delivery_short_label:
                data_type: string
                type: text
                priority: 90
                options:
                    label: orob2b.payment.system_configuration.fields.short_label.label
                    tooltip: orob2b.payment.system_configuration.fields.short_label.tooltip
                    constraints:
                        - NotBlank: ~
    
            collect_on_delivery.collect_on_delivery_sort_order:
                data_type: string
                type: text
                priority: 80
                options:
                    label: oro.collect_on_delivery.system_configuration.fields.sort_order.label
                    constraints:
                        - OroB2BBundleValidationBundleValidatorConstraintsInteger: ~
    
            collect_on_delivery.collect_on_delivery_allowed_countries:
                data_type: string
                type: choice
                priority: 70
                options:
                    label: orob2b.payment.system_configuration.fields.allowed_countries.label
                    required: true
                    constraints:
                        - NotBlank: ~
                    choice_translation_domain: messages
                    choices:
                        all: orob2b.payment.system_configuration.fields.allowed_countries.all
                        selected: orob2b.payment.system_configuration.fields.allowed_countries.selected
                    attr:
                        'data-page-component-module': 'orob2bpayment/js/app/components/config-hide-fields-component'
                        'data-dependency-id': 'collect_on_delivery_allowed_countries'
    
            collect_on_delivery.collect_on_delivery_selected_countries:
                data_type: string
                type: oro_locale_country
                priority: 60
                options:
                    label: orob2b.payment.system_configuration.fields.selected_countries.label
                    multiple: true
                    attr:
                        'data-depends-on-field': 'collect_on_delivery_allowed_countries'
                        'data-depends-on-field-value': 'selected'
    
            collect_on_delivery.collect_on_delivery_allowed_currencies:
                data_type: array
                type: oro_currency_selection
                priority: 60
                options:
                    label: orob2b.pricing.system_configuration.fields.enabled_currencies.label
                    required: true
                    multiple: true
                    constraints:
                        - NotBlank: ~
    
        tree:
            system_configuration:
                commerce:
                    children:
                        payment:
                            priority: 400
                            children:
                                collect_on_delivery:
                                    priority: 70
                                    children:
                                        collect_on_delivery_display:
                                            priority: 100
                                            children:
                                                - collect_on_delivery.collect_on_delivery_enabled
                                                - collect_on_delivery.collect_on_delivery_label
                                                - collect_on_delivery.collect_on_delivery_short_label
                                                - collect_on_delivery.collect_on_delivery_sort_order
                                                - collect_on_delivery.collect_on_delivery_allowed_countries
                                                - collect_on_delivery.collect_on_delivery_selected_countries
                                                - collect_on_delivery.collect_on_delivery_allowed_currencies

    Note: ConfigBundle guarantees that configuration from the system_configuration.yml is applied. Administrator can still manage the same settings in OroCommerce UI.

  • After that, in Oro/Bundle/CollectOnDelivery/Method/Config/CollectOnDeliveryConfigInterface.php, declare the configuration interface:
    <?php
    
        namespace OroBundleCollectOnDeliveryMethodConfig;
    
        use OroB2BBundlePaymentBundleMethodConfigPaymentConfigInterface;
        use OroB2BBundlePaymentBundleMethodConfigCountryConfigAwareInterface;
        use OroB2BBundlePaymentBundleMethodConfigCurrencyConfigAwareInterface;
    
        interface CollectOnDeliveryConfigInterface extends
            PaymentConfigInterface,
            CountryConfigAwareInterface,
            CurrencyConfigAwareInterface
        {
        }
  • Next, implement a class with the methods that read configuration option values:
    <?php
    
            namespace OroBundleCollectOnDeliveryMethodConfig;
    
            use OroBundleCollectOnDeliveryDependencyInjectionConfiguration;
            use OroBundleCollectOnDeliveryMethodCollectOnDelivery;
            use OroB2BBundlePaymentBundleDependencyInjectionConfiguration as PaymentConfiguration;
            use OroB2BBundlePaymentBundleMethodConfigAbstractPaymentConfig;
            use OroB2BBundlePaymentBundleMethodConfigCountryAwarePaymentConfigTrait;
            use OroB2BBundlePaymentBundleMethodConfigCurrencyAwarePaymentConfigTrait;
    
            class CollectOnDeliveryConfig extends AbstractPaymentConfig implements CollectOnDeliveryConfigInterface
            {
                use CountryAwarePaymentConfigTrait, CurrencyAwarePaymentConfigTrait;
    
                /** {@inheritdoc} */
                protected function getPaymentExtensionAlias()
                {
                    return CollectOnDelivery::TYPE;
                }
    
                /** {@inheritdoc} */
                public function isEnabled()
                {
                    return (bool)$this->getConfigValue(Configuration::COLLECT_ON_DELIVERY_ENABLED_KEY);
                }
    
                /** {@inheritdoc} */
                public function getOrder()
                {
                    return (int)$this->getConfigValue(Configuration::COLLECT_ON_DELIVERY_SORT_ORDER_KEY);
                }
    
                /** {@inheritdoc} */
                public function getLabel()
                {
                    return (string)$this->getConfigValue(Configuration::COLLECT_ON_DELIVERY_LABEL_KEY);
                }
    
                /** {@inheritdoc} */
                public function getShortLabel()
                {
                    return (string)$this->getConfigValue(Configuration::COLLECT_ON_DELIVERY_SHORT_LABEL_KEY);
                }
    
                /** {@inheritdoc} */
                public function isAllCountriesAllowed()
                {
                    return $this->getConfigValue(Configuration::COLLECT_ON_DELIVERY_ALLOWED_COUNTRIES_KEY)
                        === PaymentConfiguration::ALLOWED_COUNTRIES_ALL;
                }
    
                /**
                 * @inheritDoc
                 */
                public function getAllowedCountries()
                {
                    return (array)$this->getConfigValue(Configuration::COLLECT_ON_DELIVERY_SELECTED_COUNTRIES_KEY);
                }
    
                /**
                 * @inheritDoc
                 */
                public function getAllowedCurrencies()
                {
                    return (array)$this->getConfigValue(Configuration::COLLECT_ON_DELIVERY_ALLOWED_CURRENCIES);
                }
            }

    Step 3. Define a configuration service

    In Resources/config/services.yml, add service definition for the implementation above, like in the following example:

        oro_collect_on_delivery.method.config:
            class: 'OroBundleCollectOnDeliveryMethodConfigCollectOnDeliveryConfig'
            public: false
            arguments:
                - '@oro_config.manager'

    Step 4. Implement a payment method

    Note: Sources are normally grouped under the Method namespace. In our example, the CollectOnDelivery.php has OroBundlePaymentBundleMethod namespace.

    As we are extending the PaymentBundle that has dedicated PaymentMethodInterface, we are to implement the following methods:

    • execute method that is responsible for payment transaction processing (including requests to external payment gateways and updating transaction status upon their response).
      For collect on delivery, we need two actions: payment that marks payment transaction as authorized, active and successful, and capture that finalizes payment transaction and changes payment status to ‘Paid in full’.
    • getType method that returns a unique payment method id. In our sample, getType returns collect_on_delivery value.
    • isEnabled allows verifying that the payment method is enabled. Usually, this information is based on the configuration settings.
    • isApplicable allows limiting payment availability based on specific conditions (e.g. currency filter and country filter are supported in the latest version of OroCommerce).
    • supports method defines the actions supported by payment method. For the collect on delivery, supported methods are PURCHASE (an alias for AUTHORIZE) to start payment process and CAPTURE to confirm payment.

    Here is the sample implementation for the collect on delivery payment method (in Oro/Bundle/CollectOnDelivery/Method/CollectOnDelivery.php):

    <?php
    
        namespace OroBundleCollectOnDeliveryMethod;
    
        use OroBundleCollectOnDeliveryMethodConfigCollectOnDeliveryConfigInterface;
        use OroB2BBundlePaymentBundleEntityPaymentTransaction;
        use OroB2BBundlePaymentBundleMethodPaymentMethodInterface;
    
        class CollectOnDelivery implements PaymentMethodInterface
        {
            const TYPE = 'collect_on_delivery';
    
            /** @var CollectOnDeliveryConfigInterface */
            private $config;
    
            /**
             * @param CollectOnDeliveryConfigInterface $config
             */
            public function __construct(CollectOnDeliveryConfigInterface $config)
            {
                $this->config = $config;
            }
    
            /** {@inheritdoc} */
            public function execute($action, PaymentTransaction $paymentTransaction)
            {
                switch ($action) {
                    case self::PURCHASE: // on order review step create authorization transaction
                        $paymentTransaction
                            ->setAction(self::AUTHORIZE)
                            ->setActive(true)
                            ->setSuccessful(true);
                        break;
                    case self::CAPTURE: // on order view page allow to mark order paid in full
                        $paymentTransaction
                            ->setActive(false)
                            ->setSuccessful(true);
    
                        // for CAPTURE transaction sourcePaymentTransaction is  AUTHORIZE one, so lets mark it inactive after capure
                        $sourcePaymentTransaction = $paymentTransaction->getSourcePaymentTransaction();
                        if ($sourcePaymentTransaction) {
                            $sourcePaymentTransaction->setActive(false);
                        }
                        break;
                    default:
                        throw new InvalidArgumentException(sprintf('Action %s not supported', $action));
                }
    
                return [];
            }
    
            /** {@inheritdoc} */
            public function getType()
            {
                return self::TYPE;
            }
    
            /** {@inheritdoc} */
            public function isEnabled()
            {
                return $this->config->isEnabled();
            }
    
            /** {@inheritdoc} */
            public function isApplicable(array $context = [])
            {
                return $this->config->isCountryApplicable($context)
                    && $this->config->isCurrencyApplicable($context);
            }
    
            /** {@inheritdoc} */
            public function supports($actionName)
            {
                return in_array((string)$actionName, [self::PURCHASE, self::CAPTURE], true);
            }
        }

    Step 5. Define a payment method service

    In Resources/config/services.yml, for the implementation above, add service definition with orob2b_payment.payment_method tag, like in the following example:

        oro_collect_on_delivery.method:
            class: 'OroBundleCollectOnDeliveryMethodCollectOnDelivery'
            public: false
            arguments:
                - '@oro_collect_on_delivery.method.config'
            tags:
                - { name: orob2b_payment.payment_method }
    

    Step 6. Implement a view layer

    A PaymentBundle has a dedicated PaymentMethodViewInterface that call for the following methods implementation:

    • getOptions should return available configuration options. You can filter the options using context.
    • getBlock should return the block name (template) for payment method.
    • getOrder helps manage payment method order on checkout.
    • getLabel and getShortLabel should return the label or shortLabel from the configuration.
    • getPaymentMethodType returns payment method type (in our example it is ‘collect_on_delivery’).

    Here is the sample implementation for the view layer for collect on delivery payment method (in Oro/Bundle/CollectOnDelivery/Method/View/CollectOnDeliveryView.php):

        <?php
    
        namespace OroBundleCollectOnDeliveryMethodView;
    
        use OroBundleCollectOnDeliveryMethodCollectOnDelivery;
        use OroBundleCollectOnDeliveryMethodConfigCollectOnDeliveryConfigInterface;
        use OroB2BBundlePaymentBundleMethodViewPaymentMethodViewInterface;
    
        class CollectOnDeliveryView implements PaymentMethodViewInterface
        {
            /** @var CollectOnDeliveryConfigInterface */
            private $config;
    
            /**
             * @param CollectOnDeliveryConfigInterface $config
             */
            public function __construct(CollectOnDeliveryConfigInterface $config)
            {
                $this->config = $config;
            }
    
            /** {@inheritdoc} */
            public function getOptions(array $context = [])
            {
                return [];
            }
    
            /** {@inheritdoc} */
            public function getBlock()
            {
                return '_payment_methods_collect_on_delivery_widget';
            }
    
            /** {@inheritdoc} */
            public function getOrder()
            {
                return $this->config->getOrder();
            }
    
            /** {@inheritdoc} */
            public function getLabel()
            {
                return $this->config->getLabel();
            }
    
            /** {@inheritdoc} */
            public function getShortLabel()
            {
                return $this->config->getShortLabel();
            }
    
            /** {@inheritdoc} */
            public function getPaymentMethodType()
            {
                return CollectOnDelivery::TYPE;
            }
        }

    Step 7. Add a template / layout

    Finally, to enable compatibility with the new OroPlatform
    Layout feature available in the Layout Component, add a bundle-specific template and block layout in CollectOnDelivery/Resources/views/layouts/default/ folder. Create the following files:

    • A new_payment_method_block.html.twig file with the similar contents:
          {% block _payment_methods_collect_on_delivery_widget %}
          <div>
          </div>
          {% endblock %}
    • A layout.yml file with the content similar to the following:
          layout:
              actions:
                  - @setBlockTheme:
                      themes: 'CollectOnDeliveryBundle:layouts:default/collect_on_delivery_block.html.twig'

      Note: JS component is necessary for redirecting a user from the Order review step to the Finish Checkout step.

      Preview

      Once you build OroCommerce with the new payment method and enable it in OroCommerce configuration, it becomes visible in the Open Order process on the Payment step:

      When a customer submits an order, payment status changes to “Payment authorized”:

      Upon delivery and payment collection, the administrator uses Capture action that is available on the top right of the Order details screen.

      Once payment is captured, the order status changes to Paid if full.

      Source files

      Source files of the collect on delivery bundle that was used as a sample for new payment method creation are available on github.

Back to top