Edit on GitHub

MPay24

Requirement

Mpay24 PHP SDK, which can be installed by adding the composer requirement named "mpay24/mpay24-php": "^4.2".

Configuration

Inside your ecommerce.yml configuration, activate the MPay24 provider.

Implementation

CheckoutController.php (action, where the payment form will be rendered):

...
//important: if payment is active, then keep payment state and do not allow parallel payment!
if ($this->checkoutManager->hasActivePayment() && $this->cart->isCartReadOnly()) {
    return $this->redirectToRoute('app_shop_cart_list');
}
$paymentInfo = $this->checkoutManager->startOrderPayment();
$payment = $this->checkoutManager->getPayment();
$paymentFormAsString =
    $payment->initPayment(
        $this->cart->getPriceCalculator()->getGrandTotal(),
        [
            'request' => $request,
            'paymentInfo' => $paymentInfo
        ]
);
$order = $this->checkoutManager->getOrder();
$order->setOrderState("");//important to unlock order payment, as order can be locked
$order->save(['versionInfo' => 'Clear state, because payment is not locked yet.']);

$this->view->paymentFormAsString = $paymentFormAsString;

Somewhere in your order.html.php view:

...
<section>
    <h2>Select Payment:</h2>
    <?=$paymentFormAsString;?>
</section>
...

paymentMethods.html.php view (linked in ecommerce.yml):

<?php
/**
 * Partial for payment methods + form generation, called + configured in Mpay24Seamless Provider
 * @var $tokenizer
 * @var $paymentMethods string[]
 * @var $selectedMethod string
 */
// injected $selectedMethod if needed
$selectedMethod = isset($selectedMethod) ? $selectedMethod : 'cc';
?>


<form action="<?=$this->prettyUrl([],'app_shop_payment_start');?>" method="post" class="js-payment-methods-form js-cart-sidebar-update">
    <input name="token" type="hidden" value="<?php echo $tokenizer->getToken(); ?>"/>
    <? foreach ($paymentMethods as $method => $methodConfig):?>
        <div class="custom-radio">
            <label>
                <input class="custom-radio__input js-payment-method-select" type="radio" name="type" value="<?=strtoupper($method);?>"
                       data-method="<?=$method;?>"
                       required="required" <?=$method == $selectedMethod ? 'checked="checked"' : "";?>>
                <span class="custom-radio__box"></span>
                <span class="custom-radio__text"><?=$this->t('shop.payment.methods.'.$method);?></span>
            </label>
        </div>
        <? if ($method == 'cc'): ?>
            <!-- @todo frontend optimise please -->
            <div class="js-payment-method-attachment" id="payment-method-attachement-<?=$method;?>" style="<?=$selectedMethod == $method ? '' : 'display:none';?>">
                <iframe src="<?php echo $tokenizer->getLocation(); ?>" frameBorder="0"></iframe>
            </div>
        <?endif;?>
    <? endforeach;?>

    <input type="submit" href="<?=$this->prettyUrl([], 'app_shop_checkout_start');?>" class="js-payment-submit btn btn-success btn-block mt-4"
           value="<?=$this->t('shop.checkout.order.execute-payment');?>"/>

</form>

Example PaymentController.php (Responsiblity payment actions):

<?php

namespace AppBundle\Controller\Shop;

use AppBundle\Controller\AbstractController;
use AppBundle\Ecommerce\OrderManager\OrderManager;
use AppBundle\Service;
use AppBundle\TraitAware\TraitLoggerAware;
use Pimcore\Bundle\EcommerceFrameworkBundle\Factory;
use Pimcore\Bundle\EcommerceFrameworkBundle\Model\AbstractOrder;
use Pimcore\Bundle\EcommerceFrameworkBundle\PaymentManager\Payment\Mpay24Seamless;
use Pimcore\Model\Element\Note;
use Pimcore\Tool;
use Pimcore\Translation\Translator;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Routing\Annotation\Route;

class PaymentController extends AbstractController\AbstractFrontendController
{

    use TraitLoggerAware; //initializes getLogger() functionality

    /**
     * @Route("/{_prefix}/checkout/payment/start", requirements={"_prefix" = "\w\w\/?[\w\-]*"})
     * @param Request $request
     * @return RedirectResponse|\Symfony\Component\HttpFoundation\Response
     * @throws \Exception
     * @throws \Pimcore\Bundle\EcommerceFrameworkBundle\Exception\UnsupportedException
     */
    public function startAction(Request $request, RouterInterface $router)
    {
        $serviceShop = \Pimcore::getContainer()->get(Service\Shop::class);
        $checkoutManager = Factory::getInstance()->getCheckoutManager(
            $serviceShop->getCart()
        );
        /** @var Mpay24Seamless $payment */
        $payment = $checkoutManager->getPayment();
        $paymentInfo = $checkoutManager->startOrderPayment();
        $paymentType = $request->get('type');

        if ($request->isMethod('post')) {
            if ($request->isMethod('post') && $paymentType == 'INVOICE') {
                $factory = \Pimcore\Bundle\EcommerceFrameworkBundle\Factory::getInstance();
                /** @var OrderManager $orderManager */
                $orderManager = $factory->getOrderManager();
                $order = $orderManager->getOrCreateOrderFromCart($serviceShop->getCart());
                $factory->getCommitOrderProcessor()->commitOrder($order);
                return $this->redirectToCheckoutSuccessPage($order);
            } else {
                $baseUrl = Tool::getHostUrl();
                $paymentParams = [
                    'request' => $request,
                    'order' => $checkoutManager->getOrder(),
                    'paymentInfo' => $checkoutManager->getOrder()->getPaymentInfo()->get(0),
                    'successURL' => $baseUrl . $router->generate('app_shop_payment_response', ['type' => 'success', 'elementsclientauth'=>'disabled']),
                    'errorURL' => $baseUrl . $router->generate('app_shop_payment_response', ['type' => 'error', 'elementsclientauth'=>'disabled']),
                    'confirmationURL' => $baseUrl . $router->generate('app_shop_payment_response', ['type' => 'confirmation', 'elementsclientauth'=>'disabled']),
                ];
                //either redirect to paypal, etc. or to internal (error) URL
                list($redirectUrl, $errorText) = $payment->getInitPaymentRedirectUrl($paymentParams);
                if ($errorText) {
                    $this->addFlash('error', [$errorText]);
                    //save note for debugging
                    $order = $checkoutManager->getOrder();
                    $note = new Note();
                    $note->setElement($order);
                    $note->setType("user_payment_denied");
                    $note->setTitle($errorText);
                    $note->setUser(0);
                    $note->save();

                    return $this->redirectToRoute('app_shop_checkout_start');
                }
                return new RedirectResponse($redirectUrl);
            }
        }
    }

    private function redirectToCheckoutSuccessPage(AbstractOrder $order) {
        $factory = \Pimcore\Bundle\EcommerceFrameworkBundle\Factory::getInstance();
        $orderManager = $factory->getOrderManager();
        $encryptedOrderNumber = $orderManager->getEncryptedOrderNumber($order);
        return $this->redirectToRoute('app_shop_checkout_success', ['o' => $encryptedOrderNumber]);
    }

    /**
     * @Route("/{_prefix}/checkout/payment/mpay-response/{type}", requirements={"_prefix" = "\w\w\/?[\w\-]*"})
     * @param Request $request
     * @return RedirectResponse|\Symfony\Component\HttpFoundation\Response
     * @throws \Exception
     */
    public function responseAction(Request $request, Translator $translator)
    {
        $serviceShop = \Pimcore::getContainer()->get(Service\Shop::class);
        $checkoutManager = Factory::getInstance()->getCheckoutManager(
            $serviceShop->getCart()
        );

        $type = $request->get('type');
        if ($type == 'confirmation') {
            //@see https://docs.mpay24.com/docs/backend2backend-integration
            $this->getLogger()->info('Mpay24 called confirmation URL.');
            $response = new Response();
            $response->setContent("OK");
            $response->headers->set('Content-Type', 'text/plain');
            return $response;
        }

        //currently errors are also handled via commit processor and result in cancel-statements.
        //Thus, the order remains uncommitted.

        $commitOrderProcessor = Factory::getInstance()->getCommitOrderProcessor();

        /** @var Mpay24Seamless $payment */
        $payment = $checkoutManager->getPayment();

        try {
            $order = $commitOrderProcessor->handlePaymentResponseAndCommitOrderPayment(
                $request->query->all(),$payment
            );
            if ($order->getOrderState() == AbstractOrder::ORDER_STATE_COMMITTED) {
                return $this->redirectToCheckoutSuccessPage($order);
            }
        } catch (\Exception $e) {
            $this->getLogger()->error(sprintf('Exception in payment Controller: %s', $e->getMessage()));
        }

        $this->addFlash('error', [$translator->trans('shop.payment.error-or-cancelled')]);
        return $this->redirectToRoute('app_shop_checkout_start');
    }
}