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.
- execute method that is responsible for payment transaction processing (including requests to external payment gateways and updating transaction status upon their response).