SEPA Direct Debit: Technical Details
Technical structure of SEPA Direct Debit for Shopware 6.7: flow in the payment handler, services, the mmd_sepa_mandate table, migrations, templates and the admin and storefront extensions.
SEPA Direct Debit: Technical Details
Live Demo: Try this and other plugins live — no installation, no risk: demo.markus-michalski.net
Flow in the payment handler
Mmd\SepaPayment\Payment\SepaPaymentHandler extends AbstractPaymentHandler and is registered as shopware.payment.method. Payment is synchronous, there is no redirect. The handler supports neither refunds nor recurring payments (supports() always returns false).
sequenceDiagram
participant K as Customer
participant H as SepaPaymentHandler
participant S as SepaMandateService
participant D as Database
participant E as EventDispatcher
K->>H: validate - submit checkout
H->>H: Check existing mandate or check IBAN, account holder, consent
H->>H: Encrypt IBAN and pass it on in the struct
Note over K,H: Order is created
K->>H: pay
H->>S: Revoke old active mandate
H->>S: createMandate
S->>D: Store mandate
H->>E: MandateCreatedEvent
Flow in validate():
- Without a customer ID the payment is aborted.
- With
sepaUseExistingMandate=truean active mandate of the customer must exist. - Otherwise
sepaIban,sepaAccountHolderandsepaMandateAgreement=truemust be present and the IBAN valid. The IBAN is then handed topay()encrypted in a struct.
Flow in pay():
- With an existing mandate nothing more happens.
- Otherwise the handler loads order and customer, revokes an existing active mandate and creates the new one. If creating reports that the customer already has an active mandate (parallel request), the handler revokes that one and retries once.
- It then dispatches the
MandateCreatedEvent.
Components
| Class | Purpose |
|---|---|
MmdSepaPayment |
Plugin class; creates the payment method, (de)activates it and removes table and configuration on uninstall without keeping data |
Service\SepaMandateService |
Create, change, revoke and read mandates |
Service\EncryptionService |
AES-256-GCM encryption of the IBAN, key via HKDF-SHA256 from APP_SECRET |
Service\IbanValidator |
Normalization, format, length and modulo-97 check, masking |
Service\MandateReferenceGenerator |
Generates {prefix}{YYYYMMDD}-{8 characters} |
Service\MandatePdfGenerator |
Renders the Twig template documents/sepa-mandate.html.twig to an A4 PDF with dompdf |
Service\PaymentMethodInstaller |
Creates the payment method payment_sepa_direct_debit |
Subscriber\CheckoutSubscriber |
Provides the active mandate with masked IBAN as the page extension mmdSepaMandate on the checkout confirm and finish pages |
Storefront\Controller\SepaMandateController |
Customer account page, create, revoke, IBAN check, PDF downloads |
Controller\Api\SepaMandateController |
Admin routes for PDF, revocation and masked IBAN |
Event\MandateCreatedEvent |
Event for other plugins |
The services are exchangeable through the interfaces in Service\Contract, for example SepaMandateServiceInterface, IbanValidatorInterface and EncryptionServiceInterface.
Database
Table mmd_sepa_mandate (entity mmd_sepa_mandate):
| Column | Type | Note |
|---|---|---|
id |
BINARY(16) | Primary key |
customer_id |
BINARY(16) | Foreign key to customer.id, ON DELETE CASCADE |
mandate_reference |
VARCHAR(255) | Unique |
iban |
LONGTEXT | Base64 of initialization vector, auth tag and encrypted data |
bic |
VARCHAR(11) | Optional, unencrypted |
account_holder |
VARCHAR(255) | |
status |
VARCHAR(50) | active, revoked, expired; default active |
signed_at |
DATETIME(3) | |
revoked_at |
DATETIME(3) | Optional |
created_at, updated_at |
DATETIME(3) |
Indexes: unique on mandate_reference, plain on status, on customer_id and on (customer_id, status). The second migration removes the original uniqueness on customer_id. Several records per customer are therefore possible; that there is at most one active mandate is ensured by application logic, not by the database.
Where things live in the system
| What | Where |
|---|---|
| Payment method | payment_method, technical name payment_sepa_direct_debit, afterOrderEnabled off, position 10 |
| Configuration | system_config, keys MmdSepaPayment.config.* |
| Storefront texts | Snippet files mmd-sepa-payment.de-DE.json and mmd-sepa-payment.en-GB.json, namespace mmdSepaPayment |
| Admin module | mmd-sepa-mandate, menu entry under "Orders" |
Storefront and admin
- Templates: The plugin extends
payment-method.html.twig(fields at checkout), the customer account sidebar, the order details and the order finish details. - JavaScript: The storefront plugin
SepaPaymentFieldsPluginformats IBAN and BIC, validates the IBAN in the browser and passes the fields on to the order form. - Admin: List and detail page of the
mmd-sepa-mandatemodule with a PDF button; revoking on the detail page saves status and timestamp through the repository.
Notes
- The server-side modulo-97 check uses
bcmod()from the PHP bcmath extension. - The rate-limit counter of the
validate-ibanroute lives in the application cache (cache.app).