TEIF invoices, validated without Java.
TTN publishes the El Fatoora schema in XSD 1.1, which PHP cannot read. This package ships a schema PHP can read, re-implements what the conversion removes, and adds the rules the schema never checked.
composer require ayoubgaouet/tn-einvoice-validatorWhy this needed writing
libxml does not ignore XSD 1.1. It refuses.
TTN’s schema uses four XSD 1.1 constructs — two xs:assert and two xs:alternative. PHP’s libxml implements XSD 1.0, and it does not skip what it cannot parse: it declines to build the schema at all. That is why validating a TEIF invoice has meant a Java toolchain.
The four constructs are stripped by a script, not a hand edit, so a future revision can be re-processed and the delta against the official file stays auditable. What the strip removes is re-implemented as PHP rules — nothing is quietly lost.
You can also point the config straight at TTN’s original XSD 1.1 file. It is converted in memory, cached, and its remote signature import is redirected to a vendored copy so validation never touches the network.
// DOMDocument::schemaValidate() against the
// official facture_INVOIC_V1.8.8 file:
[3033] Element 'xs:element': The content is
not valid. Expected is (annotation?,
((simpleType | complexType)?, …)).
[3033] Element 'xs:extension': The content is
not valid. …
// Three schema-parse errors. Nothing in the
// invoice was examined at all.
// With this package, the same file:
XmlValidator::validate($xml)->isValid(); // trueThree layers, one pass
Every problem at once, not one per run.
A bare Xerces validator throws on the first SAXParseException, so a document with eight problems takes eight round trips. Here every layer that can run does run, and findings come back ordered by line — a fix list, read top to bottom.
Well-formedness and encoding
Is it XML at all, and is it UTF-8? If parsing fails nothing else can run, and the result says only that.
The TEIF schema
56 complex types, 30 simple types, 161 code lists, every length and occurrence constraint — enforced by libxml against the converted schema.
Business rules
Eight rule classes covering what the schema cannot express or does not check. Each names the clause of the specification it comes from.
The whole API
Three entry points and a result object.
A string, a file, or a document you have already parsed. The locale is per call, so a French API and an Arabic dashboard can share one validator.
use TnEfacture\XmlValidator\Facades\XmlValidator;
$result = XmlValidator::validate($xml);
$result = XmlValidator::validateFile($path);
$result = XmlValidator::validateDocument($dom);
// Signed invoices use the other schema profile.
XmlValidator::validate($xml, 'signed', locale: 'ar');Branch on codes, never on message text. Warnings are advice and never make a document invalid.
$result->isValid();
$result->errors(); // list<ValidationError>
$result->warnings(); // never affect validity
$result->hasError(ErrorCode::E_MF_001);
$result->translate('fr'); // re-render, no revalidation
$result->toArray();
// Each finding carries where and why:
$error->code; // 'E_DATE_002'
$error->path; // '/TEIF/InvoiceBody/Dtm/DateText'
$error->line; // 13
$error->source; // 'Guide TEIF V2.0 §5.4.4'All 18 official code lists from Annexe A, with the specification's own labels — so your dropdowns and your validation cannot drift apart.
use TnEfacture\XmlValidator\ReferenceData\Referentials;
Referentials::codes('I-13');
// ['I-131', 'I-132', … 'I-137']
Referentials::label('I-16', 'I-1602');
// 'TVA'
Referentials::has('I-13', 'ZZZ');
// false
// 18 referentials, 136 codes.Laravel is optional
Without a framework, use XmlValidator::make(). The service provider and the facade are the only classes that touch Illuminate\, and a test asserts it stays that way.
37 rules, each traced to a clause
A schema-valid invoice can still be wrong.
Much of the specification is written in the Guide and absent from the XSD. A validator that only runs the schema cannot see any of it.
<DateText functionCode="I-31" format="ddMMyy">HELLO!</DateText>
<PaiMeansCode>ZZZ</PaiMeansCode>
// This package:
// E_DATE_001 The date "HELLO!" does not match the
// declared format "ddMMyy".
// E_REF_003 The payment means code "ZZZ" is not in
// the official referential I-13.E_REF_001–003Payment codes
PaiMeansCode, PaiConditionCode and PaymentTearmsTypeCode are bound to referentials I-11, I-12 and I-13 by the Guide. The schema types all three as a plain six-character string, so any short value passes.
Guide §5.6
E_DATE_001–003Dates
DateText declares its layout in a @format attribute, but the schema types the value as an ordinary string and never compares the two. The value is checked here, along with calendar validity and period ordering.
Guide §5.4.4
E_NUM_001–002Rates and percentages
TaxRate and Percentage are typed as one-to-five-character strings with no pattern, so a tax rate of ABC is schema-valid. Monetary amounts are left alone — those the schema does check.
Guide §5.8, §5.10
E_SIG_001–009Signature structure
The XAdES-B identifiers and algorithms: the SigFrs id, exclusive canonicalisation, RSA-SHA256, SHA-256 digests, the three transforms, the certificate and the signing time — plus the rule that a submitted invoice carries exactly one signature.
Signature V3.0 §2, §5
E_XML_003Encoding
El Fatoora requires UTF-8. No schema can express that, so it is checked before anything else.
Signature V3.0 §8
E_MF_001Tax identifier format
The matricule fiscal, CIN and carte de séjour formats live in two xs:assert constructs — XSD 1.1 only, and therefore invisible to libxml. They are re-implemented as a rule.
XSD 1.8.8, Guide §5.2.1.1
Where the official documents disagree
Your data can be right and still be rejected.
The Guide describes the matricule fiscal in five parts. On three of them the schema is narrower than the Guide that documents it.
A forfaitaire taxpayer, or a secondary establishment, therefore holds a legitimate identifier that TTN’s own schema refuses. The error still stands — TTN runs the schema, so the invoice really will come back — but a warning names the exact part that diverges, so you know the problem is not your data.
| Part | Guide §5.2.1.1 | XSD 1.8.8 |
|---|---|---|
| Identifier | 7 digits | 7 digits |
| Control key | A–Z except I, O, U | A–Z except I, O, U |
| VAT code | A P B F N | A B D N P |
| Category code | M P C N E | C M N P |
| Establishment no. | 000, 001, 002… | 000 only |
W_MF_002warning“The identifier 1427326WFM000 conforms to the Guide d’Implémentation TEIF (§5.2.1.1) but is rejected by TTN’s XSD 1.8.8, which does not accept the VAT code “F”. TTN will reject this invoice.”
English, French, Arabic
The code never changes. Only the sentence does.
Even the schema errors are translated: libxml only speaks English, and in XML Schema vocabulary, so its wording is parsed back into structured parameters and re-rendered. The French follows the specification’s own terms; the Arabic follows the ones used officially in Tunisia.
enE_REF_003The payment means code “ZZZ” is not in the official referential I-13 (Payment Means).
frE_REF_003Le code de mode de paiement « ZZZ » ne figure pas dans le référentiel officiel I-13 (Payment Means).
arE_REF_003رمز وسيلة الخلاص «ZZZ» لا يوجد في المرجعية الرسمية I-13 (Payment Means).
What it does not do
The limits are documented, not buried.
This runs against invoices that carry tax liabilities and go to a government platform. Where the specification is silent or contradicts itself, the package says so rather than guessing — and a rule that is only inferred is a warning, never an error.
Signatures are checked structurally, not cryptographically
The identifiers, algorithms and required blocks are verified. Digests are not recomputed and the certificate chain is not validated — that needs canonicalisation and revocation checking, and TTN performs it on receipt. A document can pass here and still carry an invalid signature.
The amount checks are inferred, so they only warn
The Guide defines what each amount type means but states no formula anywhere — no sum, no total to reconcile, no rounding method. The arithmetic checks are derived from those definitions and hold exactly on TTN's published example, but nothing mandates them, so they never make a document invalid.
It is stricter than TTN's schema, on purpose
Fourteen of the fifty-five test fixtures are rejected here and accepted by a schema-only validator. Each one is a rule the Guide states in prose, listed with its clause in the parity manifest. If you need bare schema parity, the rules are configurable.
One documented rule is deliberately not enforced
Tableau 4 of the Guide marks LinDtm mandatory. The schema makes it optional and TTN's own published example omits it entirely, so enforcing it — even as a warning — would fire on conforming invoices.
Auto-discovered. Nothing to register.
The service provider and the XmlValidator facade register themselves through Laravel package discovery. The schemas and the three languages ship inside the package; publishing anything is optional.
composer require ayoubgaouet/tn-einvoice-validator