rezzza/sepa-bundle
Symfony bundle for generating SEPA payment files, integrating the rezzza/sepa library to produce credit transfer and direct debit XML. Provides service configuration for adding SEPA payment generation to Symfony applications.
rezzza/sepa-bundle is a Symfony bundle that generates SEPA Credit Transfer XML files (pain.001.001.03) — the batch files you hand to a bank to pay multiple European IBANs in one go. It is a thin wrapper over a SEPA XML builder, wired into Symfony's DI container.
Reality check for a Laravel codebase: this is a Symfony bundle, not a Laravel package. There is no service provider, and it predates Symfony Flex. You have two realistic paths:
If you do install it:
composer require rezzza/sepa-bundle
# inspect what it pins:
composer show rezzza/sepa-bundle
Look first at these files in vendor/rezzza/sepa-bundle/:
DependencyInjection/Configuration.php — the config keys (initiating party name, IBAN, BIC, etc.).Resources/config/services.xml — the real service IDs and constructor arguments. This is your API map.First use case — pay a list of suppliers:
// Adapt names to the actual service/builder classes in services.xml.
$sepa = $container->get('rezzza.sepa.generator'); // verify ID
foreach ($suppliers as $s) {
$sepa->addTransaction(
$s->iban,
$s->bic,
$s->name,
$s->amountCents, // keep integers, format late
'INV-'.$s->invoiceId // remittance info
);
}
$xml = $sepa->getXml(); // string ready for upload/archive
Generate one file, open it in an editor, and eyeball <GrpHdr>, <PmtInf>, and <CdtTrfTxInf> before wiring anything else.
Wrap it in your own class. Never let the bundle's classes leak through your app. Create App\Payments\SepaFileGenerator that owns the builder, formats amounts, sanitizes names, and returns a DTO with { xml, messageId, createdAt }. The package is unmaintained; the wrapper is your escape hatch to swap in digitick/sepa-xml later.
Batch as a unit of work. SEPA files are per-execution-date, per-debtor-account. Model it:
$batch = SepaBatch::forDate($executionDate, $debtorIban, $debtorBic, $initiatingName);
foreach ($payments as $p) {
$batch->add($p); // validates + sanitizes inside
}
Storage::put("sepa/{$batch->messageId()}.xml", $batch->xml());
SepaFile::create([...metadata...]); // keep the idempotency key
Idempotency via message ID. Generate a UUID (or date + sequence) for MsgId/PmtInfId and store it. If a job retries, you must not send a second file with a fresh ID — banks and reconciliation both hate that. Look up by message ID before regenerating.
One file per execution date and debtor account. Don't mix accounts inside a PmtInf. If you need to pay from two accounts, build two documents (or two PmtInf blocks if you know the schema well).
Queue it. Build + validate + store synchronously, upload asynchronously. Keep the generated XML immutable once stored — it's your audit trail.
Validate against the XSD in tests. Ship the pain.001.001.03.xsd into tests/fixtures/ and assert:
$dom = new DOMDocument();
$dom->loadXML($xml);
$this->assertTrue($dom->schemaValidate(__DIR__.'/fixtures/pain.001.001.03.xsd'));
Also snapshot-test the XML so a refactor can't silently change formatting.
Integration tip — amounts. Work in integer cents everywhere, then format at the edge:
number_format($cents / 100, 2, '.', '') // "1234.50", no separators
Integration tip — sanitization. SEPA allows a restricted Latin character set. Strip accents/diacritics and reject control chars in names and remittance info before they reach the builder:
$name = Str::ascii($name);
$name = preg_replace('/[^A-Za-z0-9 \/\-\?\:\(\)\.,\'\+]/', '', $name);
$name = mb_substr($name, 0, 70);
It is effectively abandoned. Last release 2015-12, ~6 stars, no dependents. Expect Symfony 2.x era constraints. On modern PHP/Symfony you'll likely need "config": {"platform": {"php": "..."}} gymnastics, or you fork. A Laravel app on PHP 8+ should not fight this — extract the core or use a maintained library.
Schema version is old. pain.001.001.03 is still accepted by many banks but some now require .001.09 (or ISO 20022 variants). Confirm with your bank before building on this. If they want .09, this package won't get you there without transformation.
BIC/IBAN validation is not enforced. The package builds XML; it does not guarantee your identifiers are valid. Add ramsey/uuid for IDs and a proper IBAN/BIC validator (or the Symfony Validator IBAN constraint) on your side.
No enforcement of SEPA scheme limits. Nothing stops you from emitting >999 transactions in a PmtInf, EUR-only violations, negative amounts, or missing required EndToEndId. Add your own guards:
if (count($payments) > 999) { throw new BatchTooLarge(); }
if ($currency !== 'EUR') { throw new UnsupportedCurrency(); }
Currency must be EUR. The builder may default or ignore currency; hard-code EUR and assert it.
Dates matter. Execution date must be a valid banking day; the CreDtTm timestamp should be ISO 8601 in UTC. Don't let PHP's default timezone leak into the XML.
Config quirks. The bundle config lives under rezzza_sepa: in config.yml (Symfony 2 style). If you're on Flex/Symfony 5+, that extension may never load. Read Configuration.php and set the same values directly on the builder instead of relying on the bundle's config tree.
Debugging. Dump the raw XML and validate out-of-band:
xmllint --noout --schema pain.001.001.03.xsd sepa.xml
Banks reject with terse messages referencing element paths — having the XSD locally cuts debugging time massively.
Extension points. There aren't many. You can override the bundle's service definition (Symfony) or, more practically, compose around the builder: instantiate it yourself, pass formatted data in, and keep all policy (limits, sanitization, IDs) in your adapter. Treat the package as a serializer, not a domain layer.
Migration path. Keep the output contract (XML + metadata DTO) stable and you can replace the internals with digitick/sepa-xml or a hand-rolled DOMDocument builder without touching callers. Given the package's age, plan for that swap from day one.
How can I help you explore Laravel packages today?