Implementing SEPA XML Generation in Symfony: A Step-by-Step Guide

Houssem Guemer
Active Developement
8 min readJan 5, 2024

--

SEPA XML Generation
SEPA XML Generation

SEPA (Single Euro Payments Area) XML generation is a key element in European financial transactions. It provides a uniform method for processing bank transfers in euros, making it simpler and more efficient for businesses across Europe. SEPA XML is essential for complying with European banking rules and streamlining cross-border payments.

This article is a practical guide for integrating the “php-sepa-xml/php-sepa-xml” library into a Symfony project. We aim to provide a clear, step-by-step walkthrough that developers at any level can follow. Whether you’re new to Symfony or an experienced developer, you’ll find valuable insights on how to implement SEPA XML generation in your projects. Let’s dive in and explore how to bring this crucial functionality to your Symfony applications.

Symfony setup

Symfony logo
Symfony logo

At the time of writing this article, Symfony 6.3 was the newest version and used to test this code.

Symfony installation

Before we can integrate AWS Cognito authentication with Symfony, we need to have a Symfony project set up. If you already have an existing project, you can skip this step. Otherwise, we’ll walk through the process of creating a new Symfony project.

To create a new Symfony project, we can use the command “symfony new --webapp [project-name]”. This will create a new Symfony project with the name specified

Once we have our Symfony project set up, we can verify that everything is working correctly by running the server using the command “symfony server:start”. This will start the Symfony development server and allow us to test our project

Symfony packages

In order to simplify the SEPA xml generation we will be using a library that helps us to do that.

The “php-sepa-xml/php-sepa-xml” package

Creates XML files for the Single Euro Payments Area (SEPA) Credit Transfer and Direct Debit Payments Initiation messages. These SEPA XML messages are a subset of the “ISO20022 Universal financial industry message scheme”.

We can install this package using Composer, the package manager for PHP. We simply have to run the command

composer require digitick/sepa-xml

Creating a Symfony Command

Symfony’s console component is a powerful tool for creating command-line interfaces. Here, we’ll create a Symfony command to generate SEPA XML files using the installed library. We’ll use the provided code snippet as a reference.

We can make a command easily using the following command

php bin/console make:command <command-name>

let’s say we name our command sepaXmlGenerationCommand

we will have a sepaXmlGenerationCommand.php file generated in src/command it should look like this after some modifications :

#[AsCommand(
name: 'sepa:generate',
description: 'Generate a sepa xml file',
)]
class SepaXmlGenerationCommand extends Command
{
protected function configure(): void
{}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);

$io->success('You have a new command! We will make it generate xml files next!.');

return Command::SUCCESS;
}
}

Creating a SEPA Direct Debit XML file

use Digitick\Sepa\TransferFile\Factory\TransferFileFacadeFactory;
use Digitick\Sepa\PaymentInformation;

//Set the initial information
// third parameter 'pain.008.003.02' is optional would default to 'pain.008.002.02' if not changed
$directDebit = TransferFileFacadeFactory::createDirectDebit('SampleUniqueMsgId', 'SampleInitiatingPartyName', 'pain.008.003.02');

// create a payment, it's possible to create multiple payments,
// "firstPayment" is the identifier for the transactions
// This creates a one time debit. If needed change use ::S_FIRST, ::S_RECURRING or ::S_FINAL respectively
$directDebit->addPaymentInfo('firstPayment', array(
'id' => 'firstPayment',
'dueDate' => new DateTime('now + 7 days'), // optional. Otherwise default period is used
'creditorName' => 'My Company',
'creditorAccountIBAN' => 'FI1350001540000056',
'creditorAgentBIC' => 'PSSTFRPPMON',
'seqType' => PaymentInformation::S_ONEOFF,
'creditorId' => 'DE21WVM1234567890',
'localInstrumentCode' => 'CORE', // default. optional.
// Add/Set batch booking option, you can pass boolean value as per your requirement, optional
'batchBooking' => true,
));

// Add a Single Transaction to the named payment
$directDebit->addTransfer('firstPayment', array(
'amount' => 500, // `amount` should be in cents
'debtorIban' => 'FI1350001540000056',
'debtorBic' => 'OKOYFIHH',
'debtorName' => 'Their Company',
'debtorMandate' => 'AB12345',
'debtorMandateSignDate' => '13.10.2012',
'remittanceInformation' => 'Purpose of this direct debit',
'endToEndId' => 'Invoice-No X' // optional, if you want to provide additional structured info
));
// Retrieve the resulting XML
$directDebit->asXML();

This code is a sample of how you could use the package’s DirectDebit with Factory. You can find more samples here.

Let’s implement it in our command, it should look like this:

<?php

namespace App\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Digitick\Sepa\TransferFile\Factory\TransferFileFacadeFactory;
use Digitick\Sepa\PaymentInformation;

#[AsCommand(
name: 'sepa:generate',
description: 'Generate a sepa xml file',
)]
class SepaXmlGenerationCommand extends Command
{
protected function configure(): void
{}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);

//Set the initial information
// third parameter 'pain.008.003.02' is optional would default to 'pain.008.002.02' if not changed
$directDebit = TransferFileFacadeFactory::createDirectDebit('SampleUniqueMsgId', 'SampleInitiatingPartyName', 'pain.008.003.02');

// create a payment, it's possible to create multiple payments,
// "firstPayment" is the identifier for the transactions
// This creates a one time debit. If needed change use ::S_FIRST, ::S_RECURRING or ::S_FINAL respectively
$directDebit->addPaymentInfo('firstPayment', array(
'id' => 'firstPayment',
'dueDate' => new \DateTime('now + 7 days'), // optional. Otherwise default period is used
'creditorName' => 'My Company',
'creditorAccountIBAN' => 'FI1350001540000056',
'creditorAgentBIC' => 'PSSTFRPPMON',
'seqType' => PaymentInformation::S_ONEOFF,
'creditorId' => 'DE21WVM1234567890',
'localInstrumentCode' => 'CORE', // default. optional.
// Add/Set batch booking option, you can pass boolean value as per your requirement, optional
'batchBooking' => true,
));

// Add a Single Transaction to the named payment
$directDebit->addTransfer('firstPayment', array(
'amount' => 500, // `amount` should be in cents
'debtorIban' => 'FI1350001540000056',
'debtorBic' => 'OKOYFIHH',
'debtorName' => 'Their Company',
'debtorMandate' => 'AB12345',
'debtorMandateSignDate' => '13.10.2012',
'remittanceInformation' => 'Purpose of this direct debit',
'endToEndId' => 'Invoice-No X' // optional, if you want to provide additional structured info
));

// Retrieve the resulting XML
$xmlContent = $directDebit->asXML();

// Specify the file path and name
$filePath = './var/DirectDebit.xml';

// Write the XML to the file
file_put_contents($filePath, $xmlContent);

$io->success("XML file has been generated successfully. And stored to $filePath");

return Command::SUCCESS;
}
}

and now we can run our command:

php bin/console sepa:generate

when it is done if we go to var directory we will find our debit file DirectDebit.xml which looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:pain.008.003.02" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:iso:std:iso:20022:tech:xsd:pain.008.003.02 pain.008.003.02.xsd">
<CstmrDrctDbtInitn>
<GrpHdr>
<MsgId>SampleUniqueMsgId</MsgId>
<CreDtTm>2023-11-17T15:30:55Z</CreDtTm>
<NbOfTxs>1</NbOfTxs>
<CtrlSum>5.00</CtrlSum>
<InitgPty>
<Nm>SampleInitiatingPartyName</Nm>
</InitgPty>
</GrpHdr>
<PmtInf>
<PmtInfId>firstPayment</PmtInfId>
<PmtMtd>DD</PmtMtd>
<BtchBookg>true</BtchBookg>
<NbOfTxs>1</NbOfTxs>
<CtrlSum>5.00</CtrlSum>
<PmtTpInf>
<SvcLvl>
<Cd>SEPA</Cd>
</SvcLvl>
<LclInstrm>
<Cd>CORE</Cd>
</LclInstrm>
<SeqTp>OOFF</SeqTp>
</PmtTpInf>
<ReqdColltnDt>2023-11-24</ReqdColltnDt>
<Cdtr>
<Nm>My Company</Nm>
</Cdtr>
<CdtrAcct>
<Id>
<IBAN>FI1350001540000056</IBAN>
</Id>
</CdtrAcct>
<CdtrAgt>
<FinInstnId>
<BIC>PSSTFRPPMON</BIC>
</FinInstnId>
</CdtrAgt>
<ChrgBr>SLEV</ChrgBr>
<CdtrSchmeId>
<Id>
<PrvtId>
<Othr>
<Id>DE21WVM1234567890</Id>
<SchmeNm>
<Prtry>SEPA</Prtry>
</SchmeNm>
</Othr>
</PrvtId>
</Id>
</CdtrSchmeId>
<DrctDbtTxInf>
<PmtId>
<EndToEndId>Invoice-No X</EndToEndId>
</PmtId>
<InstdAmt Ccy="EUR">5.00</InstdAmt>
<DrctDbtTx>
<MndtRltdInf>
<MndtId>AB12345</MndtId>
<DtOfSgntr>2012-10-13</DtOfSgntr>
</MndtRltdInf>
</DrctDbtTx>
<DbtrAgt>
<FinInstnId>
<BIC>OKOYFIHH</BIC>
</FinInstnId>
</DbtrAgt>
<Dbtr>
<Nm>Their Company</Nm>
</Dbtr>
<DbtrAcct>
<Id>
<IBAN>FI1350001540000056</IBAN>
</Id>
</DbtrAcct>
<RmtInf>
<Ustrd>Purpose of this direct debit</Ustrd>
</RmtInf>
</DrctDbtTxInf>
</PmtInf>
</CstmrDrctDbtInitn>
</Document>

the same way we can easily generate Credit Transfer files.

You can find some samples to do that here.

Challenges Faced and Solutions

challenge of handling monetary values

A real-world challenge I encountered during the integration of the “php-sepa-xml/php-sepa-xml” library in Symfony involved handling monetary values. In the application, payment amounts were stored as float values in euros, but the library required them in cents. The seemingly straightforward solution of converting euros to cents by multiplying by 100 and using intval() led to an unexpected issue: some amounts were off by one cent.

Understanding the Issue

The root of this problem lies in how PHP handles floating-point numbers. PHP, like many programming languages, uses a binary format to represent floating-point numbers, which can lead to precision issues. This is because not all decimal fractions can be accurately represented in binary. For example, the decimal fraction 0.1 cannot be precisely represented in binary, leading to tiny rounding errors in calculations. When dealing with currency, these tiny errors can result in significant discrepancies.

In the context of our Symfony project, when the float value representing euros was multiplied by 100 to convert it into cents, and then passed through intval(), these precision issues could result in rounding errors. This is particularly problematic in financial applications where accuracy down to the last cent is crucial.

The Solution

The solution to this problem was to use the round() function before converting the amount to an integer. Here’s how the revised code looked:

// Convert euro amount to cents and round to the nearest integer
$amountInCents = intval(round($payment->getAmount() * 100));

By first rounding the floating-point number to the nearest whole number, we could mitigate the precision issues inherent in floating-point arithmetic. This ensured that the conversion from euros to cents was accurate, and the amounts were correctly represented in the SEPA XML files.

Why PHP’s Handling of Floats Matters

This challenge underscores the importance of understanding how programming languages handle different data types. In PHP, when working with floating-point numbers, especially in the context of financial transactions, developers must be cautious about precision and rounding. Using functions like round() appropriately is crucial to avoid subtle yet critical errors in calculations.

This experience serves as a valuable reminder of the complexities involved in handling numerical data in software development, particularly in financial applications where accuracy is paramount.

Best Practices: Handling Sensitive Financial Data

types in PHP

When it comes to financial applications, accuracy and precision are paramount. A critical best practice is to store and manage monetary data in its most precise format. In the context of our Symfony project and the integration of the “php-sepa-xml/php-sepa-xml” library, this means handling monetary values in cents rather than euros.

Why Store in Cents?

  1. Avoids Floating-Point Precision Issues: As previously discussed, floating-point arithmetic can introduce small but significant errors. By storing values in cents (integer format), we eliminate the risks associated with floating-point precision.
  2. Simplifies Calculations: Dealing with integers makes calculations straightforward and less prone to errors. When monetary values are stored in cents, operations like summing up amounts or applying discounts become more reliable and predictable.
  3. Uniformity Across the Application: Consistency in data format is crucial for maintaining code clarity and reducing complexity. By uniformly using cents throughout the application, developers can avoid confusion and reduce the likelihood of introducing bugs during data manipulation.

Conclusion

Euro

This article has journeyed through the process of integrating the “php-sepa-xml/php-sepa-xml” library into a Symfony project for SEPA XML generation.

Using Symfony, a robust and flexible PHP framework, along with the “php-sepa-xml/php-sepa-xml” library, offers a powerful combination for handling financial transactions. Symfony’s structure and features like the console component greatly facilitate the implementation of complex tasks, such as SEPA XML file generation. The “php-sepa-xml/php-sepa-xml” library further simplifies this process by providing a specialized tool tailored for SEPA compliance, ensuring that applications meet the required standards for European financial transactions.

--

--