Markus Michalski FAQ

SEPA Direct Debit: Developer API

Developer reference for SEPA Direct Debit on Shopware 6.7: MandateCreatedEvent, SepaMandateServiceInterface with all methods, admin API routes for PDF, revocation and masked IBAN, and the storefront routes.

Deutsche Version

Back to overview

SEPA Direct Debit: Developer API

Live Demo: Try this and other plugins live — no installation, no risk: demo.markus-michalski.net

The plugin gives other plugins three integration points: an event for new mandates, a service for accessing mandates, and HTTP routes for admin and storefront.

MandateCreatedEvent

Property Value
Event name mmd.sepa_payment.mandate.created
Class Mmd\SepaPayment\Event\MandateCreatedEvent
Constant MandateCreatedEvent::EVENT_NAME

The event is dispatched after a new mandate has been stored while an order is completed. It is not dispatched when an existing mandate is reused, and not when a customer creates their mandate on the customer account page.

The event is dispatched under its name. Subscribe to the string mmd.sepa_payment.mandate.created; a subscription by class name is not triggered.

Method Return Description
getMandateId() string UUID of the new mandate
getCustomerId() string UUID of the customer
getOrderId() string UUID of the order during which the mandate was created
getContext() Context Shopware context
getMandate() ?SepaMandateEntity The stored mandate; its IBAN is encrypted

Example: event subscriber

<?php

namespace MyPlugin\Subscriber;

use Mmd\SepaPayment\Event\MandateCreatedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class SepaMandateSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            'mmd.sepa_payment.mandate.created' => 'onMandateCreated',
        ];
    }

    public function onMandateCreated(MandateCreatedEvent $event): void
    {
        $mandate = $event->getMandate();

        if ($mandate === null) {
            return;
        }

        // Example: hand the mandate data to an ERP system
        $reference = $mandate->getMandateReference();
        $holder = $mandate->getAccountHolder();
        $orderId = $event->getOrderId();
    }
}

Optional dependency

If your plugin should only optionally support the SEPA plugin, use the event name as a string and type the parameter as object, so there is no hard dependency on the event class:

public function onMandateCreated(object $event): void
{
    $mandateId = $event->getMandateId();
}

SepaMandateServiceInterface

Mmd\SepaPayment\Service\Contract\SepaMandateServiceInterface is registered as a service (alias to SepaMandateService) and can be injected via the constructor.

Method Return Description
createMandate(string $customerId, array $data, Context $context) string Creates a mandate and returns its ID. $data: iban, accountHolder, optional bic. Throws InvalidArgumentException for an invalid IBAN, account holder or BIC, and RuntimeException if the customer already has an active mandate. Dispatches no event.
updateMandate(string $mandateId, array $data, Context $context) void Changes bic and accountHolder. The IBAN cannot be changed.
revokeMandate(string $mandateId, Context $context) void Sets the status to revoked and records revokedAt. The mandate is kept as a record.
getMandateByCustomerId(string $customerId, Context $context, bool $activeOnly = true) ?SepaMandateEntity A customer's mandate. With activeOnly = false revoked ones are included; with several matches the order is not defined.
getMandateById(string $mandateId, Context $context) ?SepaMandateEntity Mandate by UUID
hasActiveMandate(string $customerId, Context $context) bool Does the customer have an active mandate?
getDecryptedIban(SepaMandateEntity $mandate) string Decrypted IBAN. Throws RuntimeException if decryption fails, for example after APP_SECRET has changed.
getMaskedIban(SepaMandateEntity $mandate) string Masked IBAN in the format country code, check digits, ****, last four characters, for example DE89****3000

The SepaMandateEntity entity offers, among others, getMandateReference(), getAccountHolder(), getBic(), getStatus(), getSignedAt(), getRevokedAt(), getCustomerId(), isActive() and isRevoked(). getIban() returns the encrypted IBAN only. getMaskedIban() on the entity always throws an exception; use the service instead.

Example: reading a customer's mandate

use Mmd\SepaPayment\Service\Contract\SepaMandateServiceInterface;
use Shopware\Core\Framework\Context;

class MyService
{
    public function __construct(
        private readonly SepaMandateServiceInterface $mandateService,
    ) {
    }

    public function describe(string $customerId, Context $context): ?array
    {
        $mandate = $this->mandateService->getMandateByCustomerId($customerId, $context);

        if ($mandate === null) {
            return null;
        }

        return [
            'reference' => $mandate->getMandateReference(),
            'ibanMasked' => $this->mandateService->getMaskedIban($mandate),
            'accountHolder' => $mandate->getAccountHolder(),
            'signedAt' => $mandate->getSignedAt(),
        ];
    }
}

The decrypted IBAN is personal bank account data. Call getDecryptedIban() only when you really need it, and never write it to logs.

Admin API routes

All routes are in the API scope and require an admin login.

Method Path Privilege Response
GET /api/_action/mmd-sepa-mandate/{mandateId}/pdf mmd_sepa_mandate.viewer PDF file; 404 with {"error": "Mandate not found"}
POST /api/_action/mmd-sepa-mandate/{mandateId}/revoke mmd_sepa_mandate.editor {"success": true}; 404 if unknown, 400 if the mandate is not active
GET /api/_action/mmd-sepa-mandate/{mandateId}/masked-iban mmd_sepa_mandate.viewer {"maskedIban": "DE89****3000"}; 404 if unknown

The plugin does not register the privileges mmd_sepa_mandate.viewer and mmd_sepa_mandate.editor in the admin role management. The routes are therefore only usable for administrators with full access.

In addition, the mmd_sepa_mandate entity is reachable through the admin's standard entity API.

Storefront routes

Method Path Route name Purpose
GET /account/sepa-mandate frontend.account.sepa.mandate Mandate page (login required)
POST /account/sepa-mandate/create frontend.account.sepa.mandate.create Create a mandate (login required)
POST /account/sepa-mandate/revoke frontend.account.sepa.mandate.revoke Revoke the active mandate (login required)
POST /account/sepa-mandate/validate-iban frontend.account.sepa.mandate.validate-iban Returns {"valid": true} or {"valid": false}; login required, XHR; at most 10 requests per minute per IP, then 429
GET /account/sepa-mandate/download frontend.account.sepa.mandate.download PDF of the customer's own mandate (login required)
GET /sepa-mandate/download/{orderId} frontend.sepa.mandate.download.order PDF via the order; owner or deepLinkCode as query parameter

The bundled checkout script does not call the validate-iban route; it validates the IBAN in the browser.

Orders and mandates

The plugin stores no link between an order and a mandate. The mandate belongs to the customer. You only learn the order from the MandateCreatedEvent via getOrderId().

Integration examples

The event is suited to report mandate data to your own systems, such as an ERP, a CRM or accounting software, or to trigger a notification. The plugin does not ship such integrations; you build them yourself on top of the event.