CFONB 120 statements
CFONB 120 is the French bank statement format (relevé de compte, also known as ETEBAC 3): one record of 120 characters per line, its first two characters being the record code.
- 01opening balance
- 04operation, repeated
- 05detail of the operation above, optional
- 07closing balance
| Record | Parsed into | Reached with |
|---|---|---|
01 |
Balance |
$statement->getOldBalance() |
04 |
Operation |
$statement->getOperations() |
05 |
OperationDetail |
$operation->getDetails() |
07 |
Balance |
$statement->getNewBalance() |
Reading a file
Section titled “Reading a file”Cfonb120Reader::parse() takes the contents of the file, not its path, and returns a list of
Statement:
use Silarhi\Cfonb\Cfonb120Reader;
$reader = new Cfonb120Reader();$content = file_get_contents('/path/to/cfonb120-file.txt');
foreach ($reader->parse($content) as $statement) { // Process each statement echo "Statement date: " . $statement->getNewBalance()->getDate()->format('Y-m-d') . "\n";}The examples of this page parse this file:
0110278 02204EUR2 00012345603 060420 0000000166956E0604200704200410556677202204EUR2 00012345603B1070420 070420PRLV SEPA ONLINE SAS 0000000000000000000227R0510556677202204EUR2 00012345603B1070420 LIBDEDIBOX 37061140710278 02204EUR2 00012345603 070420 0000000166728F0110278 02204EUR2 00020427603 070420 0000000166728F0704200804200410556677202204EUR2 00020427603B1080420 080420PRLV SEPA FREE MOBILE 0000000000000000000201J0410278000202204EUR2 0002042760305080420 080420VIR JOHNDOE / FOOBAR 0000000000000000050000{0410278085902204EUR2 0002042760306080420 080420VIR BOR TEST / FOOBAR 0000000000000000000201J01/01/2025.20710278 02204EUR2 00020427603 080420 0000000216326D0110278 02204EUR2 00020427603 080420 0000000216326D0804200904200710278 02204EUR2 00020427603 090420 0000000216326DHow statements are built
Section titled “How statements are built”The reader goes through the lines in order:
- an old balance line (
01) opens a statement. - an operation line (
04) is added to the open statement. - a detail line (
05) is added to the last operation read. - a new balance line (
07) closes the open statement, which is then added to the list: the next operation line needs a new01line. - an empty line is ignored; a line of spaces or tabs throws a
ParseException.
Any other order throws a ParseException, in strict and non-strict mode alike: a 07 line, an operation or a detail
line with no open statement, a 01 line while a statement is still open, a statement still open at the end of the file
(a truncated file).
<?php
use Silarhi\Cfonb\Cfonb120Reader;use Silarhi\Cfonb\Exceptions\ParseException;
$reader = new Cfonb120Reader();$lines = file('statements.txt', FILE_IGNORE_NEW_LINES);
// A truncated file: the "07" new balance of the last statement is missingtry { $reader->parse(implode("\n", array_slice($lines, 0, -1)));} catch (ParseException $e) { echo $e->getMessage(), "\n";}
// A "04" operation before the "01" old balance of its statementtry { $reader->parse(implode("\n", [$lines[1], $lines[0], $lines[3]]));} catch (ParseException $e) { echo $e->getMessage(), "\n";}Unable to close the statement of account 00020427603 opened on 2020-04-08: missing "07" new balance record at the end of the fileUnable to attach the operation with code B1 dated 2020-04-07: missing "01" old balance record before itSo every statement returned has both balances: hasOldBalance() and hasNewBalance() are always true on them, and
getOldBalance() and getNewBalance() never throw. A bank usually sends one statement per account and per day: a file
can hold several days and several accounts, in the order the bank wrote them.
Operations
Section titled “Operations”<?php
use Silarhi\Cfonb\Cfonb120Reader;
$reader = new Cfonb120Reader();$statements = $reader->parse(file_get_contents('statements.txt'));
// The first operation of the file, a SEPA direct debit with one detail line$operation = $statements[0]->getOperations()[0];
var_dump([ 'bankCode' => $operation->getBankCode(), 'deskCode' => $operation->getDeskCode(), 'accountNumber' => $operation->getAccountNumber(), 'internalCode' => $operation->getInternalCode(), 'currencyCode' => $operation->getCurrencyCode(), 'code' => $operation->getCode(), 'date' => $operation->getDate()->format('Y-m-d'), 'valueDate' => $operation->getValueDate()->format('Y-m-d'), 'rejectCode' => $operation->getRejectCode(), 'label' => $operation->getLabel(), 'reference' => $operation->getReference(), 'exemptCode' => $operation->getExemptCode(), 'amount' => $operation->getAmount(), 'bankOperationReference' => $operation->getBankOperationReference(),]);
foreach ($operation->getDetails() as $detail) { echo sprintf("Detail %s: %s\n", $detail->getQualifier(), $detail->getAdditionalInformations());}array(14) { ["bankCode"]=> string(5) "10556" ["deskCode"]=> string(5) "02204" ["accountNumber"]=> string(11) "00012345603" ["internalCode"]=> string(4) "6772" ["currencyCode"]=> string(3) "EUR" ["code"]=> string(2) "B1" ["date"]=> string(10) "2020-04-07" ["valueDate"]=> string(10) "2020-04-07" ["rejectCode"]=> NULL ["label"]=> string(20) "PRLV SEPA ONLINE SAS" ["reference"]=> string(7) "0000000" ["exemptCode"]=> string(1) "0" ["amount"]=> float(-22.79) ["bankOperationReference"]=> NULL}Detail LIB: DEDIBOX 3706114getCode()is the operation code of the CFONB,B1here: operation codes gives its label.getDate()is the operation date,getValueDate()the value date.- Fields left blank in the file are
nullfor the nullable getters (getRejectCode(),getBankOperationReference()above); the others throw aParseExceptionwhen blank, except the reference in non-strict mode. getDetails()lists the05lines of the operation, in order: theirgetQualifier()(LIBfor an extra label line) tells whatgetAdditionalInformations()holds.
All the fields, with their positions in the line: CFONB 120 objects.
foreach ($reader->parse($content) as $statement) { foreach ($statement->getOperations() as $operation) { echo "Date: " . $operation->getDate()->format('Y-m-d') . "\n"; echo "Value Date: " . $operation->getValueDate()->format('Y-m-d') . "\n"; echo "Amount: " . $operation->getAmount() . "\n"; echo "Label: " . $operation->getLabel() . "\n"; echo "Reference: " . $operation->getReference() . "\n"; echo "Bank Code: " . $operation->getBankCode() . "\n"; echo "Account: " . $operation->getAccountNumber() . "\n";
// Optional fields are null when blank if (null !== $operation->getInternalCode()) { echo "Internal Code: " . $operation->getInternalCode() . "\n"; }
// Operation details (additional information) foreach ($operation->getDetails() as $detail) { echo "Additional Info: " . $detail->getAdditionalInformations() . "\n"; } }}Amounts
Section titled “Amounts”Balance and operation amounts are floats, signed: credits are positive, debits are negative. In the file, an amount is 13 digits and a sign character, which also holds the last digit; the number of decimals comes from position 20 of the line:
| Last character | Sign | Last digit |
|---|---|---|
{, A…I |
positive | 0, 1…9 |
}, J…R |
negative | 0, 1…9 |
0000000166956E with 2 decimals is 16695.65, with 0 decimals 1669565; 0000000000227R with 2 decimals is
-22.79.
Dates are DateTimeImmutable objects at midnight, in the default time zone of PHP, read from DDMMYY: years 00 to
69 are 2000 to 2069, 70 to 99 are 1970 to 1999.
A date that does not exist throws a ParseException instead of rolling over to the next month: day 00 or 32, month
00 or 13, 31 April, 29 February of a non-leap year. 320420 fails with Unable to parse date "320420".
Every CFONB 120 date is mandatory, the value date of an operation included. Some banks write a date they do not have
as zeros: 000000 fails with Unable to parse date "000000": the date is zero-filled (empty), but this field is mandatory, in strict and non-strict mode alike.
Strict mode (default) vs non-strict mode
Section titled “Strict mode (default) vs non-strict mode”In strict mode, any line that cannot be parsed throws a ParseException. Non-strict mode only relaxes one rule: a blank
operation reference on a 04 line becomes an empty string instead of an error. Every other error is still thrown, the
order of the records and zero-filled dates included.
use Silarhi\Cfonb\Cfonb120Reader;use Silarhi\Cfonb\Exceptions\ParseException;
$reader = new Cfonb120Reader();
// Strict mode (default) - throws an exception on parsing errorstry { $statements = $reader->parse($content); // strict=true by default} catch (ParseException $e) { echo "Parsing error: " . $e->getMessage() . "\n";}
// Non-strict mode - blank operation references are returned as ''$statements = $reader->parse($content, false); // strict=falseWith a file whose two operations have no reference:
0110278 02204EUR2 00020427603 070420 0000000166728F0704200804200410556677202204EUR2 00020427603B1080420 080420PRLV SEPA FREE MOBILE 0000000000201J0410278000202204EUR2 0002042760306080420 080420VIR JOHNDOE / FOOBAR 0000000050000}0710278 02204EUR2 00020427603 080420 0000000116527E<?php
use Silarhi\Cfonb\Cfonb120Reader;use Silarhi\Cfonb\Exceptions\ParseException;
$reader = new Cfonb120Reader();$content = file_get_contents('blank-references.txt');
// Strict mode (the default): a blank operation reference is an errortry { $reader->parse($content);} catch (ParseException $e) { echo sprintf("Strict: %s\n", $e->getMessage());}
// Non-strict mode: a blank operation reference becomes an empty stringforeach ($reader->parse($content, false) as $statement) { foreach ($statement->getOperations() as $operation) { echo sprintf("Non-strict: %s, reference %s\n", $operation->getLabel(), var_export($operation->getReference(), true)); }}Strict: The value of key "reference" is not a stringNon-strict: PRLV SEPA FREE MOBILE, reference ''Non-strict: VIR JOHNDOE / FOOBAR, reference ''