Implement Direct Debit in Zed

Edit on GitHub

This document provides shows how to implement the Direct Debit payment method and integrate it into Checkout, State Machine, and OMS on the backend side.

Persist payment details

The payment details for the Direct Debit payment method must be persisted in the database.

To persist the payment details, do the following:

1. Create a new table to store payment details data

Add the spy_payment_direct_debit.schema.xml file with the following content to the Persistence/Propel/Schema/ folder in Zed:

<?xml version="1.0"?>
<database xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="zed"
	xsi:noNamespaceSchemaLocation="http://static.spryker.com/schema-01.xsd"
	namespace="Orm\Zed\PaymentMethods\Persistence" package="src.Orm.Zed.PaymentMethods.Persistence">

<table name="spy_payment_direct_debit">
	<column name="id_payment_directdebit" type="INTEGER" autoIncrement="true" primaryKey="true"/>
	<column name="fk_sales_order" required="true" type="INTEGER"/>

	<column name="bank_account_holder" type="VARCHAR" />
	<column name="bank_account_bic" type="VARCHAR" size="100"/>
	<column name="bank_account_iban" size="50"  type="VARCHAR"/>

	<foreign-key name="spy_payment_directdebit-fk_sales_order" foreignTable="spy_sales_order">
		<reference foreign="id_sales_order" local="fk_sales_order"/>
	</foreign-key>

	<behavior name="timestampable"/>
	<id-method-parameter value="spy_payment_directdebit_pk_seq"/>
    </table>

    </database>

2. Perform a database migration and generate the query object:

Run the following command:

vendor/bin/console propel:install

3. Save the Direct Debit payment details in the persistence layer:

  1. Create the PaymentMethodsPersistenceFactory class on the persistence layer:
<?php
namespace Pyz\Zed\PaymentMethods\Persistence;

use Orm\Zed\PaymentMethods\Persistence\SpyPaymentDirectDebitQuery;
use Spryker\Zed\Kernel\Persistence\AbstractPersistenceFactory;

/**
* @method \Pyz\Zed\PaymentMethods\Persistence\PaymentMethodsQueryContainer getQueryContainer()
*/
class PaymentMethodsPersistenceFactory extends AbstractPersistenceFactory
{
	/**
	* @return \Orm\Zed\PaymentMethods\Persistence\SpyPaymentDirectDebitQuery
	*/
	public function createPaymentDirectDebitQuery()
	{
		return SpyPaymentDirectDebitQuery::create();
	}

}
  1. Implement PaymentMethodsQueryContainer:
<?php
namespace Pyz\Zed\PaymentMethods\Persistence;

use Spryker\Zed\Kernel\Persistence\AbstractQueryContainer;

/**
* @method  \Pyz\Zed\PaymentMethods\Persistence\PaymentMethodsPersistenceFactory getFactory()
*/
class PaymentMethodsQueryContainer extends AbstractQueryContainer
{
	/**
	* @param int $idSalesOrder
	*
	* @return \Orm\Zed\PaymentMethods\Persistence\SpyPaymentDirectDebitQuery
	*/
	public function queryPaymentBySalesOrderId($idSalesOrder)
	{
		return $this->getFactory()->createPaymentDirectDebitQuery()->filterByFkSalesOrder($idSalesOrder);
	}
}

Save Direct Debit payment details

To add the logic for saving and viewing the Direct Debit payment details on the business layer and expose them using PaymentMethodsFacade, do the following:

  1. In the Business/Reader/ folder, add the DirectDebitReader class. This implements the logic for viewing the Direct Debit payment details.
Code sample
<?php

namespace Pyz\Zed\PaymentMethods\Business\Reader;


use Generated\Shared\Transfer\PaymentDirectDebitTransfer;
use Pyz\Zed\PaymentMethods\Persistence\PaymentMethodsQueryContainer;

class DirectDebitReader
{

	/**
	* @var \Pyz\Zed\PaymentMethods\Persistence\PaymentMethodsQueryContainer
	*/
	protected $queryContainer;

	/**
	* @param \Pyz\Zed\PaymentMethods\Persistence\PaymentMethodsQueryContainer $queryContainer
	*/
	public function __construct(PaymentMethodsQueryContainer $queryContainer)
	{
		$this->queryContainer = $queryContainer;
	}

	/**
	* @param int $salesOrderId
	*
	* @return \Generated\Shared\Transfer\PaymentDirectDebitTransfer
	*/
	public function getPaymentForOrderId($salesOrderId)
	{
		$entity = $this->queryContainer->queryPaymentBySalesOrderId($salesOrderId)->findOne();
		$directDebitTransfer = new PaymentDirectDebitTransfer();
		$directDebitTransfer->fromArray($entity->toArray(), true);

		return $directDebitTransfer;
	}

	/**
	* @param int $salesOrderId
	*
	* @return bool
	*/
	public function hasPaymentForOrderId($salesOrderId)
	{
		$entity = $this->queryContainer->queryPaymentBySalesOrderId($salesOrderId)->findOne();

		return $entity !== null;
	}

}
  1. In the Business/Writer/ folder, add the DirectDebitWriter class. This implements the logic for saving the Direct Debit payment details.
Code sample:
<?php

namespace Pyz\Zed\PaymentMethods\Business\Writer;

use Generated\Shared\Transfer\CheckoutResponseTransfer;
use Generated\Shared\Transfer\QuoteTransfer;
use Orm\Zed\PaymentMethods\Persistence\SpyPaymentDirectDebit;
use Pyz\Shared\PaymentMethods\PaymentMethodsConstants;

class DirectDebitWriter
{
	/**
     * @param \Generated\Shared\Transfer\QuoteTransfer $quoteTransfer
     * @param \Generated\Shared\Transfer\SaveOrderTransfer $saveOrderTransfer
     *
     * @return void
     */
    public function saveOrderPayment(QuoteTransfer $quoteTransfer, SaveOrderTransfer $saveOrderTransfer)
    {
        if ($quoteTransfer->requirePayment()->getPayment()->requirePaymentMethodsDirectDebit()->getPaymentMethod() == PaymentMethodsConstants::PAYMENT_METHOD_NAME_DIRECT_DEBIT) {
            $this->saveDirectDebit($quoteTransfer, $saveOrderTransfer);
        }
    }

    /**
     * @param \Generated\Shared\Transfer\QuoteTransfer $quoteTransfer
     * @param \Generated\Shared\Transfer\SaveOrderTransfer $saveOrderTransfer
     *
     * @return void
     */
    protected function saveDirectDebit(QuoteTransfer $quoteTransfer, SaveOrderTransfer $saveOrderTransfer)
    {
        $entity = new SpyPaymentDirectDebit();

        $directDebitTransfer = $quoteTransfer->requirePayment()->getPayment()->requirePaymentMethodsDirectDebit()->getPaymentmethodsdirectdebit();

        $entity->fromArray($directDebitTransfer->toArray());
        $entity->setFkSalesOrder($saveOrderTransfer->getIdSalesOrder());

        $entity->save();
    }
}
  1. To get instances for these two classes, implement PaymentMethodsBusinessFactory

Code sample:

<?php

namespace Pyz\Zed\PaymentMethods\Business;

use Pyz\Zed\PaymentMethods\Business\Reader\DirectDebitReader;
use Pyz\Zed\PaymentMethods\Business\Writer\DirectDebitWriter;
use Spryker\Zed\Kernel\Business\AbstractBusinessFactory;

class PaymentMethodsBusinessFactory extends AbstractBusinessFactory
{
	/**
	* @return \Pyz\Zed\PaymentMethods\Business\Writer\DirectDebitWriter
	*/
	public function createDirectDebitWriter()
	{
		return new DirectDebitWriter();
	}

	/**
	* @return \Pyz\Zed\PaymentMethods\Business\Reader\DirectDebitReader
	*/
	public function createDirectDebitReader()
	{
		return new DirectDebitReader($this->getQueryContainer());
	}

}
  1. Expose the save/retrieve Direct Debit payment details using PaymentMethodsFacade:
Code sample:
<?php

namespace Pyz\Zed\PaymentMethods\Business;

use Generated\Shared\Transfer\CheckoutResponseTransfer;
use Generated\Shared\Transfer\QuoteTransfer;
use Spryker\Zed\Kernel\Business\AbstractFacade;

/**
* @method \Pyz\Zed\PaymentMethods\Business\PaymentMethodsBusinessFactory getFactory()
*/
class PaymentMethodsFacade extends AbstractFacade
{

	/**
     * @param \Generated\Shared\Transfer\QuoteTransfer $quoteTransfer
     * @param \Generated\Shared\Transfer\SaveOrderTransfer $saveOrderTransfer
     *
     * @return void
     */
    public function saveOrderPayment(QuoteTransfer $quoteTransfer, SaveOrderTransfer $saveOrderTransfer)
    {
        $this->getFactory()->createDirectDebitWriter()->saveOrderPayment($quoteTransfer, $saveOrderTransfer);
    }

	/**
	* @param int $idSalesOrder
	*
	* @return \Generated\Shared\Transfer\PaymentDirectDebitTransfer
	*/
	public function getOrderDirectDebit($idSalesOrder)
	{
		return $this->getFactory()->createDirectDebitReader()->getPaymentForOrderId($idSalesOrder);
	}

	/**
	* @param int $idSalesOrder
	*
	* @return bool
	*/
	public function hasOrderDirectDebit($idSalesOrder)
	{
		return $this->getFactory()->createDirectDebitReader()->hasPaymentForOrderId($idSalesOrder);
	}

}

Listen to Direct Debit payment details in Zed UI

In Zed, when looking over the details on a submitted order, check the payment details.

To view the payment details, do the following:

  1. Extend the order details page by adding the PaymentMethods/Presentation/Sales/list.twig template with the following content:

Code sample:

{% if (idPayment) %}
	<div class="row">
		<div class="col-sm-12">
			{% embed '@Gui/Partials/widget.twig' with { widget_title: 'Direct Debit' | trans } %}
				{% block widget_content %}
					<table class="footable table table-striped toggle-arrow-tiny">
						<tbody>
						<tr>
							<td><strong>{{ 'Account Holder' | trans }}</strong></td>
                            <td>{{ paymentDetails.bankAccountHolder }}</td>
                            </tr>
						    <tr>
                            <td><strong>{{ 'IBAN' | trans }}</strong></td>
							<td>{{ paymentDetails.bankAccountIban }}</td>
						</tr>
						<tr>
                            <td><strong>{{ 'BIC' | trans }}</strong></td>
							<td>{{ paymentDetails.bankAccountBic }}</td>
                        </tr>
                        </tbody>
                    </table>
				{% endblock %}
			{% endembed %}
        </div>
    </div>
{% endif %}
  1. In PaymentMethods/Communication/Controller/SalesController.php, add the corresponding controller action for this view:

Code sample:

<?php

namespace Pyz\Zed\PaymentMethods\Communication\Controller;

use Spryker\Zed\Kernel\Communication\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;

/**
* @method \Pyz\Zed\PaymentMethods\Persistence\PaymentMethodsQueryContainer getQueryContainer()
* @method \Pyz\Zed\PaymentMethods\Business\PaymentMethodsFacade getFacade()
*/
class SalesController extends AbstractController
{

	/**
	* @param \Symfony\Component\HttpFoundation\Request $request
	*
	* @return array
	*/
	public function listAction(Request $request)
	{
		$idSalesOrder = $request->query->get('id-sales-order');

		if ($this->getFacade()->hasOrderDirectDebit($idSalesOrder)) {
			$directDebitTransfer = $this->getFacade()->getOrderDirectDebit($idSalesOrder);

			return [
				'idPayment' => $directDebitTransfer->getIdPaymentDirectdebit(),
				'paymentDetails' => $directDebitTransfer,
			];
		}

		return [
			'idPayment' => null,
			'paymentDetails' => null,
		];
	}
}

Information is available here: /payment-methods/sales/list?id-sales-order=1

Integrate the Direct Debit method into the checkout

To integrate the Direct Debit method into the checkout, implement these plugins:

  • PaymentMethodsDirectDebitCheckoutPreConditionPlugin
  • PaymentMethodsDirectDebitCheckoutDoSaveOrderPlugin
  • PaymentMethodsDirectDebitCheckoutPostSavePlugin

To do this, take the following steps:

  1. In Zed, add the following 3 plugins to the Communication/Plugin/Checkout/ folder of the new module you’ve created (PaymentMethods).

PaymentMethodsDirectDebitCheckoutPreConditionPlugin

<?php

namespace Pyz\Zed\PaymentMethods\Communication\Plugin\Checkout;

use Generated\Shared\Transfer\CheckoutResponseTransfer;
use Generated\Shared\Transfer\QuoteTransfer;
use Spryker\Zed\CheckoutExtension\Dependency\Plugin\CheckoutPreConditionPluginInterface;
use Spryker\Zed\Kernel\Communication\AbstractPlugin;


class PaymentMethodsDirectDebitCheckoutPreConditionPlugin extends AbstractPlugin implements CheckoutPreConditionPluginInterface

{
  /**
   * @param \Generated\Shared\Transfer\QuoteTransfer $quoteTransfer
   * @param \Generated\Shared\Transfer\CheckoutResponseTransfer $checkoutResponseTransfer
   *
   * @return bool
   */
  public function checkCondition(QuoteTransfer $quoteTransfer, CheckoutResponseTransfer $checkoutResponseTransfer)
  {
      checkoutResponseTransfer->setIsSuccess(true);

      return true;
  }
}

PaymentMethodsDirectDebitCheckoutDoSaveOrderPlugin

<?php

namespace Pyz\Zed\PaymentMethods\Communication\Plugin\Checkout;

use Generated\Shared\Transfer\QuoteTransfer;
use Generated\Shared\Transfer\SaveOrderTransfer;
use Spryker\Zed\CheckoutExtension\Dependency\Plugin\CheckoutDoSaveOrderInterface;
use Spryker\Zed\Kernel\Communication\AbstractPlugin;

/**
 * @method \Pyz\Zed\PaymentMethods\Business\PaymentMethodsFacade getFacade()
 */
class PaymentMethodsDirectDebitCheckoutDoSaveOrderPlugin extends AbstractPlugin implements CheckoutDoSaveOrderInterface
{
    /**
     * @param \Generated\Shared\Transfer\QuoteTransfer $quoteTransfer
     * @param \Generated\Shared\Transfer\SaveOrderTransfer $saveOrderTransfer
     *
     * @return void
     */
    public function saveOrder(QuoteTransfer $quoteTransfer, SaveOrderTransfer $saveOrderTransfer)
    {
        $this->getFacade()->saveOrderPayment($quoteTransfer, $saveOrderTransfer);
    }
}


PaymentMethodsDirectDebitCheckoutPostSavePlugin

<?php

use Generated\Shared\Transfer\CheckoutResponseTransfer;
use Generated\Shared\Transfer\QuoteTransfer;
use Spryker\Zed\CheckoutExtension\Dependency\Plugin\CheckoutPostSaveInterface;
use Spryker\Zed\Kernel\Communication\AbstractPlugin;

/**
 * @method \Pyz\Zed\PaymentMethods\Business\PaymentMethodsFacadeInterface getFacade()
 * @method \Pyz\Zed\PaymentMethods\PaymentMethodsConfig getConfig()
 */
class PaymentMethodsDirectDebitCheckoutPostSavePlugin extends AbstractPlugin implements CheckoutPostSaveInterface
{
    /**
     * @param \Generated\Shared\Transfer\QuoteTransfer $quoteTransfer
     * @param \Generated\Shared\Transfer\CheckoutResponseTransfer $checkoutResponseTransfer
     *
     * @return \Generated\Shared\Transfer\CheckoutResponseTransfer
     */
    public function executeHook(QuoteTransfer $quoteTransfer, CheckoutResponseTransfer $checkoutResponseTransfer)
    {
        return $checkoutResponseTransfer;
    }
}
  1. In the Checkout module, add the previous three plugins to the related methods (plugin stacks) in CheckoutDependencyProvider.
Code sample
<?php
namespace Pyz\Zed\Checkout;

use Pyz\Zed\PaymentMethods\Communication\Plugin\Checkout\PaymentMethodsDirectDebitCheckoutPreConditionPlugin;
use Pyz\Zed\PaymentMethods\Communication\Plugin\Checkout\PaymentMethodsDirectDebitCheckoutDoSaveOrderPlugin;
use Pyz\Zed\PaymentMethods\Communication\Plugin\Checkout\PaymentMethodsDirectDebitCheckoutPostSavePlugin;
use Spryker\Zed\Checkout\CheckoutDependencyProvider as SprykerCheckoutDependencyProvider;
use Spryker\Zed\Kernel\Container;


class CheckoutDependencyProvider extends SprykerCheckoutDependencyProvider
{
    /**
     * @param \Spryker\Zed\Kernel\Container $container
     *
     * @return \Spryker\Zed\CheckoutExtension\Dependency\Plugin\CheckoutPreConditionPluginInterface[]
     */
    protected function getCheckoutPreConditions(Container $container)
    {
        return [
            ...
            new PaymentMethodsDirectDebitCheckoutPreConditionPlugin(),
        ];
    }
    /**
     * @param \Spryker\Zed\Kernel\Container $container
     *
     * @return \Spryker\Zed\Checkout\Dependency\Plugin\CheckoutSaveOrderInterface[]|\Spryker\Zed\CheckoutExtension\Dependency\Plugin\CheckoutDoSaveOrderInterface[]
     */
    protected function getCheckoutOrderSavers(Container $container)
    {
        return [
            ...
            new PaymentMethodsDirectDebitCheckoutDoSaveOrderPlugin(),
        ];
    }
    /**
     * @param \Spryker\Zed\Kernel\Container $container
     *
     * @return \Spryker\Zed\CheckoutExtension\Dependency\Plugin\CheckoutPostSaveInterface[]
     */
    protected function getCheckoutPostHooks(Container $container)
    {
        return [
            ...
            new PaymentMethodsDirectDebitCheckoutPostSavePlugin(),
        ];
    }
}

Configure dependency injectors for Yves and Zed

Add injectors for Zed and Yves and ActiveProcess and Statemachine mapping to the config\Shared\config_default.php file:

// ---------- Dependency injector
$config[KernelConstants::DEPENDENCY_INJECTOR_YVES] = [
	'CheckoutPage' => [
		PaymentMethodsConstants::PROVIDER,
	],
];
$config[KernelConstants::DEPENDENCY_INJECTOR_ZED] = [
	'Oms' => [
		PaymentMethodsConstants::PROVIDER,
	],
];
// ---------- State machine (OMS)
$config[OmsConstants::ACTIVE_PROCESSES] = [
	PaymentMethodsConstants::PAYMENT_METHOD_DIRECTDEBIT,
];
$config[SalesConstants::PAYMENT_METHOD_STATEMACHINE_MAPPING] = [
	PaymentMethodsConstants::PAYMENT_METHOD_DIRECTDEBIT => PaymentMethodsConstants::PAYMENT_METHOD_DIRECTDEBIT,
];

Integrate the Direct Debit Payment Method into a state machine

After the preceding procedures have been completed, set up a state machine, which is dedicated for processing orders that use Direct Debit as a payment type. For this purpose, add the paymentMethodsDirectDebit.xml file with the following content to the config/Zed/oms/ folder:

Code sample
<?xml version="1.0"?>
<statemachine
    xmlns="spryker:oms-01"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="spryker:oms-01 http://static.spryker.com/oms-01.xsd">

    <process name="DirectDebit" main="true">
    <states>
        <state name="new" reserved="true"/>
        <state name="payment issued" />
        <state name="payment received" />
        <state name="order shipped" />
        <state name="ready for return" />
        <state name="completed" />
        <state name="cancelled" />
        <state name="refunded" />
        <state name="returned" />
        <state name="payment clarification needed" />
    </states>

    <transitions>
        <transition>
            <source>new</source>
        <target>payment issued</target>
            <event>capture direct debit</event>
        </transition>

        <transition>
            <source>payment issued</source>
            <target>payment received</target>
        <event>payment received</event>
        </transition>

        <transition>
            <source>payment issued</source>
            <target>payment clarification needed</target>
    <event>clarify payment</event>
        </transition>

        <transition>
            <source>payment clarification needed</source>
            <target>cancelled</target>
            <event>cancel</event>
        </transition>

        <transition>
            <source>payment received</source>
            <target>order shipped</target>
            <event>ship order</event>
        </transition>

        <transition>
            <source>order shipped</source>
            <target>ready for return</target>
            <event>ready for return</event>
        </transition>

        <transition>
            <source>ready for return</source>
            <target>completed</target>
            <event>item not returned</event>
        </transition>

        <transition>
            <source>new</source>
            <target>cancelled</target>
            <event>cancel</event>
        </transition>

        <transition>
            <source>payment received</source>
            <target>refunded</target>
            <event>refund</event>
        </transition>

        <transition>
            <source>refunded</source>
            <target>cancelled</target>
            <event>cancel</event>
        </transition>

        <transition>
            <source>returned</source>
            <target>refunded</target>
            <event>refund</event>
        </transition>

        <transition>
            <source>ready for return</source>
            <target>returned</target>
            <event>return received</event>
        </transition>
    </transitions>

    <events>
        <event name="capture Direct Debit" manual="true" />
        <event name="payment received" manual="true" />
        <event name="ship order" manual="true" />
        <event name="ready for return"  onEnter="true" />
        <event name="item not returned" timeout="30days" />
        <event name="cancel" manual="true" />
        <event name="refund" manual="true" />
        <event name="return received" manual="true" />
        <event name="clarify payment" manual="true" />
    </events>
   </process>
</statemachine>

What’s next?

After creating and integrating the Direct Debit payment method into the backend, identify the new Direct Debit payment type in the shared layer.