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.
| 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 |
<?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();
}
}
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();
}
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.
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.
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.viewerandmmd_sepa_mandate.editorin 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.
| 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.
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().
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.