Skip to content

Changelog

What’s new, version by version, straight from CHANGELOG.md on GitHub.

0.3.0

Latest

Release on GitHub

Added

  • Single PHP rules can be switched off, re-rated or replaced by their code: validation.rules.disabled, validation.rules.severity and Quittio::extend('rule', …) in Laravel, RuleOverrides with Quittio::configureRules() in the core. Official EN 16931 and Peppol rules need allow_official; unknown codes are refused. Quittio::ruleCodes() lists the known codes.
  • The calculation policy decides what happens with declared amounts of other systems: calculation.declared_amounts (keep, recalculate, reject), CalculationPolicy and DeclaredAmountsMode in the core. An own calculator is configured with calculation.calculator or passed to Quittio::create(calculator: …); Quittio::calculator() returns it.
  • Quittio::edit() (core and Laravel) and InvoiceBuilder::fromUsing() continue existing invoice data with the calculator of the instance; cancellations and the document calculator of QuittioFake use it as well. InvoiceFactory::calculated() takes a calculator.
  • Tax presets whose rate depends on the date (Quittio\Core\Tax\DatedTaxPreset) are resolved with the supply date of each line (SupplyDate: line period, VAT point date, delivery date, invoicing period, issue date). DatedValues holds values that change on dates; TaxPresetCollection::alias() finds presets under further keys.
  • VatPreset::Standard (de.standard) and VatPreset::Reduced (de.reduced) take the German rate of the supply date (TaxTables), e.g. 16 % and 5 % from July to December 2020. The plausibility rule DE-VAT-RATE warns about rates that applied neither as standard nor as reduced rate on the supply date. The small amount limit of DE-USTG-08 is that of the issue date (150.00 EUR before 2017).
  • Country packs register output formats, aliases and serializers (Countries\Capabilities\ProvidesFormats, implemented by AbstractCountryPack); the German pack brings XRechnung (XRechnungFormats). Profiles declare sub lines, extension fields and third party payments with capabilities (SupportsSubLines, SupportsExtensionFields, SupportsThirdPartyPayments) instead of the core checking XRechnung keys.
  • Default interest for every country pack with a reference rate: Quittio\Core\Dunning\DefaultInterest (forCountry($pack)), the contract Dunning\ReferenceRates and the capability ProvidesReferenceRates; the German pack provides its base rates (BaseRateTable implements ReferenceRates) and its DefaultInterest uses the core calculation. DocumentBuilder::interestCharge() adds calculated charges with their day count and texts.
  • Documents can be queued in Laravel: Quittio::document($quote)->queue()->save($disk, $path) with the job RenderDocument, the events QueuedDocumentStored and QueuedDocumentFailed and QuittioManager::queueDocument(); QuittioFake records them (assertDocumentQueued(), queuedDocuments()). Documents are stored as versioned snapshots (DocumentData::toSnapshot(), fromSnapshot(), toArray(), toJson() …) with their own upcasters (quittio.snapshots.document_upcasters, SnapshotUpcasterChain::documentDefault()); Mapping\Attributes\MapOf keeps keyed arrays.
  • The German special cases are reached with the builder macro de(): ->de()->smallAmount(), ->de()->selfBilled(), ->de()->deductAdvance($advance) … (GermanInvoice).

Changed

  • Lines without VAT treatment of a German seller get the preset key de.standard instead of de.standard_19, and quittio.defaults.vat = 'de.reduced_7' gives de.reduced. Applications that compare preset keys use VatPreset::fromKey($key)?->canonical(). Supplies from 2020-07-01 to 2020-12-31 get 16 % and 5 %.
  • CiiWriter, UblWriter, CiiReader and UblReader keep no state between calls: each call works with its own internal worker, so one instance can be used reentrantly. The readers no longer extend the internal XPathReader and therefore no longer expose its constants.
  • The render run resolves the logo of the presentation before it builds the view model (ViewContext::$logo, ResolvedLogo), so the view layer does no I/O; ViewModelFactory keeps no state between calls.
  • The documentation website is versioned: the root shows the latest release, /next/ the development version and /X.Y/ older minor versions, with a version switcher (docs/_versioning/build.mjs). Every version has a generated API reference under api/ (phpDocumentor, tools/docs/phpdoc.sh); the builder traits under Builder\Concerns are no longer @internal, so their methods appear there.
  • Pull requests opened in the read-only split repositories quittio/core, quittio/laravel and quittio/country-de are closed automatically with a pointer to the monorepo.

Deprecated

  • VatPreset::Standard19 and VatPreset::Reduced7: use VatPreset::Standard and VatPreset::Reduced; the keys de.standard_19 and de.reduced_7 stay readable. Flags::SMALL_AMOUNT_LIMIT: use TaxTables::smallAmountLimitOn().
  • The builder macros smallAmount(), propertyRelatedService(), newVehicle(), selfBilled(), deductAdvance() and consumerDefaultNotice() directly on the builder: use ->de()->…. GermanInvoiceMacros::ADVANCE_TYPES: use GermanInvoice::ADVANCE_TYPES. FormatRegistry::XRECHNUNG_CII and XRECHNUNG_UBL: use XRechnungFormats::CII and UBL of the German pack, which registers the formats; the core keeps them until 1.0.

Fixed

  • Peppol invoices with an electronic address scheme that Peppol does not accept, e.g. EM (e-mail), are rejected with the new rule PEPPOL-EN16931-CL008 instead of passing the PHP rules and failing the official Peppol validation. The allowed schemes are the code list CodeList::PEPPOL_EAS (peppol-eas-codes.json, from the Peppol BIS rules 3.0.20); tools/codelists/update.php regenerates it with CodeListGenerator::generatePeppol().

0.2.0

Release on GitHub

Added

  • The legal texts of the German pack (LegalTexts) and the text of default interest come from its language files (resources/lang/{de,en}/legal.php), so lang/vendor/quittio-de overrides them in Laravel; LegalTexts::DEFAULT_INTEREST is new.
  • quittio/country-de ships stubs/InvoiceBuilder.stub.php, which describes its builder macros (smallAmount(), selfBilled(), deductAdvance(), …) for IDEs and PHPStan.
  • Tests: generated round trips through CII and UBL, fuzzing of the XML, PDF and array input, and tests for formats, mapping, serialisation, the PDF parser, mail attachments, a second boot and the escaping of the Blade templates.
  • Documentation: Versioning and compatibility with the backwards compatibility promise; ARCHITECTURE.md, decision records in docs/adr/, RELEASING.md, a reworked CONTRIBUTING.md, an FAQ page; issue and pull request templates, CODEOWNERS and response times in SECURITY.md. Classes outside the public API (XPathReader, XmlBuilder, Pdf\Parsing, CarryOver, ConfigMapper, the command and preview classes, …) are marked @internal.
  • Documents (quotes, reminders, …) dispatch DocumentRendering and DocumentRendered in Laravel; PendingDocument has option() (core and Laravel), data(), options() and is Macroable (Laravel).
  • Quittio::setPostProcessor() replaces the PDF/A and XML embedding for renderers that cannot embed. Profiles describe their embedded XML with the capability DescribesEmbeddedXml (XMP description and fx:Version) instead of the file name deciding it.
  • Amounts of prepaid(), addPrepaid(), rounding(), discountPercent(), discountAmount() and of the document builder's fee() and openingBalance() accept BigDecimal, int or string like line prices (floats are rejected). DocumentBuilder::note(), discountPercent() and discountAmount() take the same subject and reason codes as the invoice builder.
  • QuittioFake dispatches the events of a real run (InvoiceRendering, InvoiceValidated, InvoiceRendered) and records queued invoices: assertQueued(), assertNothingQueued(), queued(). Its decisions follow configuration changes without Quittio::flush().
  • Events QueuedInvoiceStored (a queued job stored the file, with disk and path) and QueuedInvoiceFailed (a queued job failed for good).
  • Quittio\Core\Quittio::resetState() forgets the process-wide state (translator bridge, default snapshot upcasters, application default VAT, base rates of the German pack); extensions register their own state with onResetState(). The test trait InteractsWithQuittio calls it after each test.

Changed

  • README and documentation mark the Managed layer quittio/managed as not released yet; its documentation page is a preview and is left out of llms.txt, llms-full.txt and the MCP index.
  • The documentation is built and link-checked on pull requests; every PHP code block of the documentation must parse.
  • quittio/core, quittio/laravel and quittio/country-de declare their test dependencies and ship a phpunit.xml.dist, so their split repositories run composer test on their own.
  • quittio/quittio registers the Quittio facade alias and suggests ext-intl, ext-zip and opis/json-schema like the single packages.
  • Development: Composer scripts test:fast, test:official, test:visual, test:coverage, test:mutation, docs:generate, codelists:update and ci; a weekly CI run with composer audit, line coverage and mutation testing; PHPStan also checks tools/*.php and reports calls of deprecated code.
  • Visual baselines are written only with QUITTIO_UPDATE_BASELINES=1; a missing baseline fails the test. The default Blade partials that print pre-escaped HTML carry a warning comment.
  • Queued RenderInvoice jobs store the render options as versioned JSON (RenderOptions::toArray()/fromArray()) instead of a PHP-serialised object, so an update of Quittio no longer breaks jobs in the queue; jobs queued by 0.1.0 still run. Errors that another attempt cannot fix (validation, invalid data, configuration) fail the job at once instead of three times; other errors are retried after 10 and 60 seconds. Inside a database transaction the job is dispatched after the commit (quittio.queue.after_commit).
  • Every exception that Quittio throws implements QuittioException: invalid arguments and runtime errors are now Quittio\Core\Exceptions\InvalidArgumentException, RuntimeException or LogicException, which extend the PHP classes of the same name, so existing catch blocks keep working.
  • Country pack discovery keeps its result in bootstrap/cache/quittio-countries.php (and per process) until Composer changes the installed packages, instead of reading vendor/composer/installed.json whenever the core is built.
  • The Laravel configuration no longer defaults to Germany: defaults.country (QUITTIO_COUNTRY) and seller.address.country are null, which means the only installed country pack (Germany with quittio/quittio), so existing applications keep their behaviour. With several packs, set defaults.country or the seller's country. Quittio::registerCountry() makes the next call build the core again.
  • The Laravel manager builds a new core when the quittio configuration changes, so Quittio::flush() is no longer needed after config()->set(). Under Octane it reads the configuration of the current request instead of the application that created the singleton; so does the default VAT of lines without VAT.

Deprecated

  • Environment variables are named after their configuration keys: QUITTIO_DEFAULT_COUNTRY, QUITTIO_VALIDATION_MODE, QUITTIO_MANDATORY_FIELD_GUARD, QUITTIO_STORAGE_DISK and QUITTIO_PREVIEW_ENABLED. The names of 0.1.0 (QUITTIO_COUNTRY, QUITTIO_VALIDATION, QUITTIO_GUARD, QUITTIO_DISK, QUITTIO_PREVIEW) are still read when the new one is not set, until 1.0.
  • The methods of Quittio\Core\Quittio that change the instance are named after what they do: addSerializer(), addValidator(), clearValidators(), setRenderer(), setTemplateEngine(), setPaymentQrGenerator() and setFormatResolver(). The with…() names, which elsewhere in Quittio return a changed copy, remain as deprecated aliases until 1.0.

Security

  • quittio:schemas:download checks the KoSIT validator, the XRechnung configuration, the Peppol rules and SchXslt against pinned SHA-256 checksums before it unpacks or runs them; so do the download scripts in tools/.
  • Decimal strings are only accepted in plain notation (Decimal::of(), builders, InvoiceData::fromArray()/fromJson(), snapshots): an exponent such as 1e999999999 was expanded to a billion digits and exhausted the memory. Other notations throw a NumberFormatException (a HydrationException for array and JSON input).
  • The preview route refuses requests outside the local environment unless the gate viewQuittioPreview allows them; before, QUITTIO_PREVIEW=true made it public behind the web middleware only. Requests are limited to 30 per minute (quittio.preview.throttle). QuittioServiceProvider::registerPreviewRoutes() is no longer public.

Fixed

  • Default interest of payment reminders was described in English for German locales with a region such as de_AT.
  • InvoiceData::fromJson() threw PHP's JsonException for JSON that cannot be parsed (empty, too deep, invalid UTF-8); it now throws a HydrationException. JsonSchemaValidator::validate() reports such input as a finding.
  • DE-USTG-08 names the limit of Flags::SMALL_AMOUNT_LIMIT (250.00 EUR) instead of a second, hard-coded value. The English fallback messages of four UStG rules matched their translations no longer; a test now compares every rule's fallback with lang/en/rules.php and requires a German message.
  • The field reference documents the UBL credit note path of BT-129 and BT-130 (cbc:CreditedQuantity) and no longer shows pseudo-XPath for BT-110 and BT-111. A test now evaluates every documented CII and UBL path against the XML of the sample invoices and of the official XRechnung test suite.
  • A custom TotalsCalculator was bypassed by the VAT alignment of partial cancellations; Cancellation::of() and DeclaredAmounts::needed() accept the calculator, and Quittio::cancellation() passes its own.
  • Texts with control characters (e.g. a vertical tab copied from a spreadsheet cell) or invalid UTF-8 produced XML that was not well-formed and was rejected by every receiver. The new rule QUITTIO-INVALID-CHARACTERS names the field, and the CII and UBL writers refuse such data with an InvalidInvoiceData exception.
  • A UBL credit note without payment instructions (BG-16) left out its due date (BT-9) silently: the UBL binding transmits it only in cac:PaymentMeans. The new warning QUITTIO-CREDIT-NOTE-DUE-DATE points this out; the field reference names the credit note path.
  • A percentage document allowance or charge without base amount whose VAT category and rate matched no invoice line became 0.00 silently (data from JSON, readers or with()); the calculator now throws a CalculationException. Drafts without any lines still calculate with a zero base.
  • Reading an invoice and the check for declared amounts no longer turn errors such as a TypeError of a custom TotalsCalculator into the warning QUITTIO-READER-RECALCULATION; only exceptions are reported.
  • The post-processing of PDFs from renderers that cannot embed (PDF/A and XML) uses the configured temp_dir instead of the system temporary directory.
  • quittio:validate: an unknown --profile printed a stack trace; files that cannot be checked (missing, not an e-invoice, unknown profile) now exit with 2 instead of 1, so scripts can tell them from invalid files; with --json such errors are printed as {"valid": false, "error": …} and the result names expectedProfile and profileMismatch.
  • Failed writes are reported with a clear message: quittio:render, quittio:preview and quittio:base-rate:update exit with 1, RenderedInvoice::save() of the Laravel package throws a RuntimeException when the disk does not store the file. save() of the core results and quittio:render --output treat a path ending in / as directory and create missing directories (RenderedDocument::save() did not create them).
  • The CII and UBL readers accept every xs:boolean spelling of ChargeIndicator (1/0, surrounding white space). Before, such document allowances and charges were dropped and line charges were read as allowances; a missing or invalid indicator is now reported as QUITTIO-READER-CHARGE-INDICATOR. The documented CII and UBL paths of BG-20 and BG-21 are valid XPath.

0.1.0

Release on GitHub

First release.

Added

  • Monorepo quittio/quittio (the complete package) with the read-only splits quittio/core, quittio/laravel and quittio/country-de.
  • EN 16931 data model with mapping attributes (BT, cardinality, profile level, CII and UBL paths), builders, JSON conversion, generated JSON schema and field reference.
  • Calculation according to EN 16931 including document allowances split by VAT rate, gross prices, foreign currency and third party payments.
  • Profiles MINIMUM, BASIC WL, BASIC, EN 16931, EXTENDED, Peppol BIS 3; XRechnung 3.0 and the XRechnung extension in the country pack Germany.
  • Validation levels L1 to L7: PHP rules (EN 16931, Peppol, BR-DE, UStG), XSD, KoSIT validator (daemon and CLI), veraPDF (CLI and Docker), plausibility checks.
  • CII and UBL writers and readers with protection against XXE, entity expansion and oversized input; PDF reader for embedded invoices with decompression limits.
  • ZUGFeRD/Factur-X PDF/A-3b with mPDF, XMP metadata and associated files; post processing of foreign PDFs; EPC GiroCode.
  • View model with formatted and raw values, mandatory field guard, plain PHP and Blade default templates with partials, DIN 5008 letter layouts.
  • Format resolver, entry point Quittio\Core\Quittio and Laravel facade with several formats per call, storage, downloads, mail attachments, queue, events, preview route and Artisan commands.
  • Quotes, order confirmations, delivery notes, proforma invoices, payment reminders and account statements.
  • Country pack Germany: tax presets, UStG rules, format rules, legal texts, BMF error classes, numbering, retention, dunning law with base rate table, correction policy, builder helpers.
  • Testing helpers: fake, assertions, invoice factory, sample scenarios, visual regression (identical baselines on macOS and the Linux CI image).
  • quittio:schemas:download compiles the Peppol BIS Billing 3.0 rules; KoSIT configuration accepts several scenario sets.
  • Base rates up to 1 July 2026; small business invoices according to section 34a UStDV.
  • Full cancellations (Quittio::cancellation()) with configurable sign strategy: type code 381 with positive amounts (default) or 380 with negative amounts (corrections.cancellation_strategy).
  • Quittio::prepare() / PendingInvoice::prepare(): calculation, format and profile choice and validation without rendering.
  • InvoiceBuilder::updateLine(), removeLine() and InvoiceLine::withNetPrice().
  • Complete reference documentation: new pages Building invoices, Laravel integration and Configuration; every builder method, config key (both packages), command, event, exception, tax preset and rule is documented, the Managed layer page covers models, numbering, archive, immutability, delivery, audit log, authorisation and retention in full.
  • Documentation website built from docs/ with Jekyll and Just the Docs (navigation, search), deployed to Cloudflare by .github/workflows/docs.yml; the Markdown files stay the only source.
  • Country pack Germany: tax preset VatPreset::TriangularTransaction for intra-community triangular transactions (section 25b UStG, wording of section 14a (7) UStG).
  • Country pack Germany: rule DE-USTG-11 requires the VAT identifiers of seller and buyer for reverse charge services in another member state and for triangular transactions (section 14a (1) and (7) UStG); the tax number or a legal registration identifier is not sufficient there.
  • Country pack Germany: ->deductAdvance($advanceInvoice) for final invoices (section 14 (5) sentence 2 UStG): references the paid advance invoice, adds it to BT-113 and itemises its net amounts and VAT per rate in a note. InvoiceBuilder::addPrepaid() adds to BT-113.
  • quittio.seller.small_business now takes effect: Seller::apply() marks the invoice (Meta::FLAG_SMALL_BUSINESS) and the German pack rejects VAT on it (DE-USTG-09).
  • Official KoSIT test for every German tax preset in XRechnung CII and UBL; the XRechnung golden-file tests run in the official CI job; CI validates every package composer.json.
  • Laravel: format_resolver.peppol_lookup (invokable class or callable receiving the BuyerParty) for the automatic Peppol choice; Quittio::coreWithLevels(); RenderedDocument::hasPdf(); Quittio::fake()->documents().
  • Core: LineBuilder::showGrossPrice(); InvoiceBuilder::payee() accepts a PayeeParty, identifier schemes or a PartyBuilder callable; Quittio::cancellation() takes an optional $title; country pack readers (CountryPack::readers()) are used by read() and validateFile().
  • Country pack Germany: info DE-LEITWEG-ID-SUSPECT for buyer references shaped like a Leitweg-ID with wrong check digits; warning DE-USTG-13 when newVehicle() is used without a line of category K.
  • Country pack Germany: the base rate (§ 247 BGB) can be updated without a release: quittio-de.base_rates in the new configuration config/quittio-de.php, php artisan quittio:base-rate:update fetching it from the statistics API of the Deutsche Bundesbank (BundesbankBaseRates), or BaseRates::register(); DefaultInterest uses these rates by default. The pack has a Laravel service provider of its own (GermanyServiceProvider, discovered automatically).
  • Country pack Germany: tax numbers (BT-32) of German sellers are checked for format and check digit according to the ELSTER procedures of every federal state (warning DE-TAX-NUMBER); TaxNumber parses the ELSTER, federal and state notations.
  • Country pack Germany: rule DE-USTG-14 rejects intra-community supplies (category K) of small businesses except for new vehicles (section 19 (1) sentence 2 and (6) sentence 1 UStG); warning DE-USTG-15 for invoices on intra-community supplies and on services in another member state issued after the 15th of the month following the supply (section 14a (1) sentence 2 and (3) sentence 1 UStG); UstgRules::issuingDeadline().
  • Documentation for AI assistants: llms.txt, llms-full.txt and every page as Markdown on the website, a read-only MCP server at https://docs.quittio.dev/mcp (tools search_docs, get_page, list_pages) and Laravel Boost guidelines and the skill quittio-development in quittio/quittio and quittio/laravel.
  • Core: shorter invoice code. Lines can be given as name with named values (->line('Consulting', netPrice: '120.00', quantity: 4, unit: UnitCode::Hour), also vat, description and priceInclVat; the same in Line::make(), subLine() and the document builder), as Line::make('Consulting', …)->… or as arrays whose keys are the LineBuilder methods (LineBuilder::ARRAY_KEYS, fill(), Line::fromArray()), parties as Party::make('Kunde AG')->… or as application models: ->buyer($customer) for models implementing ProvidesParty. subLine() and the document builder accept the same forms.
  • Core: default VAT for lines without vat(): InvoiceBuilder::defaultVat() and DocumentBuilder::defaultVat() per invoice, InvoiceBuilder::useDefaultVat() for the application, CountryDefaultVat (configured preset, otherwise the standard preset of the seller's country pack, for small business invoices its small business preset), CountryDefaults::$vatPreset and $smallBusinessVatPreset (Germany: de.standard_19, de.small_business), CountryRegistry::taxPreset().
  • Core: InvoiceBuilder::applyDefaults() remembers the defaults of a seller; seller() with another seller removes them again, except values the caller set. Quittio::invoice() and the document methods use it, so another seller gets neither the bank account nor the small business flag (nor, with quittio/managed, the payment terms, design, locale, time zone and currency) of the configured seller.
  • validation.locale: the language of validation messages, independent of the invoice language. In Laravel the default is the language of the application (app()->getLocale()), 'invoice' restores the language of the invoice; without Laravel the default stays the invoice language, 'app' uses the translator's locale. ValidationContext::messageLocale().
  • Laravel: Quittio::invoice()->…->check() validates without rendering.
  • Core: fewer callbacks. dueInDays(), cashDiscount() and lateFee() on the invoice and document builder replace paymentTerms(fn ($t) => …); presentation() takes an array of changes; updateLine('2', netPrice: '80.00', quantity: 3) takes named values; payee() takes a PartyBuilder. The callbacks keep working.
  • Laravel: QUITTIO_SELLER_SMALL_BUSINESS for quittio.seller.small_business.
  • Laravel: config/quittio.php explains every key; pdf.colors.secondary is listed (default null, as before).
  • PDF: carry-over across pages. When the line table continues on the next page, the page ends with "Fortsetzung auf Seite 2 · Übertrag" and the sum of the lines so far, the next page starts with "Übertrag von Seite 1" (English "Continued on page 2 · Carried forward", "Brought forward from page 1"). The mPDF renderer lays a document out again only when the lines span pages; own templates enable it with markers (templates), view model lineGroups[].lines[].carryOver. Following pages get a header with logo or name, title and number, recipient, customer number and date.
  • Fonts: Geist (SIL Open Font License) is bundled and the default font, with the family geistmedium (Medium, SemiBold) for medium weights; Inter stays bundled (pdf.font = 'inter', also for Greek).
  • View model: design.fontMedium, design.continuationMarginTop, design.logoMaxHeight, design.footerHasTaxIds, totals.dueEqualsGross; labels payment, transfer_until, remittance_short, vat_row_single, your_vat_id, your_tax_number, tax_number_short, carry_over, carry_over_from, continued_on.
  • Documentation for first-time users: new pages E-invoicing basics (EN 16931, CII and UBL, formats, profiles, rule codes, which format you need, German terms) and Troubleshooting (common errors with cause and fix); Getting started shows the minimal configuration of your company and explains a first invoice line by line; every page starts with what it is for, explains its terms and is ordered from the common case to the reference; German text in the English documentation is translated or explained.
  • Laravel: quittio.defaults.vat (tax preset of lines without ->vat(), default the standard rate of the seller's country) and RenderedInvoice::cancel($number, reason: …), which renders the cancellation in the formats of the invoice; Quittio::defaultVat().
  • Laravel: Quittio::invoice() starts an invoice with the seller, bank account and small business setting of the configuration and renders it with ->render() (optionally a format or a list of formats); Quittio::quote(), orderConfirmation(), deliveryNote(), proforma(), paymentReminder() and accountStatement() start documents the same way. The seller comes from the binding of Quittio\Laravel\Contracts\InvoiceDefaults (default ConfigInvoiceDefaults; quittio/managed binds the current issuer).
  • Documentation page Examples: complete examples from service invoices, small businesses, XRechnung and customers abroad to cancellations, Laravel orders, drafts, reminders and incoming invoices; a test runs the code of the page, and another one checks that every code block of the documentation imports the classes it uses.

Changed

  • Country pack Germany: DE-USTG-01 names the party and the missing address fields ("Die Anschrift des leistenden Unternehmers (Verkäufer, BG-5) ist unvollständig, es fehlt: Straße und Hausnummer, Postleitzahl, Ort") and says where to fill them in. The message of ValidationException contains the hint of every listed finding.
  • Core: macros of InvoiceBuilder, LineBuilder and DocumentBuilder run in the scope of the builder class, so they reach its state also on subclasses.
  • Core: a line without quantity() has the quantity 1 instead of failing with BT-129.
  • Repository layout: the root package is quittio/quittio (it replaces quittio/core, quittio/laravel and quittio/country-de); the meta package was removed; the packages are split into read-only repositories on every push; the commercial quittio/managed lives in its own repository.
  • Country pack Germany: the format rule only decides for public buyers (Leitweg-ID with valid check digits or endpoint 0204: XRechnung, also for sellers abroad) and domestic small amount invoices (PDF); consumers, the Peppol lookup, domestic B2B (ZUGFeRD with the configured profile), sellers outside Germany and buyers in Büsingen or Heligoland are left to the format resolver.
  • check() and prepare() always validate (whatever validation.mode or withoutValidation() say), validate the CII XML that pdf embeds with b2c_embed_xml exactly as render() writes it, and refuse XML formats for watermarked data like render().
  • Laravel: InvoiceValidated is no longer dispatched in validation mode off; the unused networks config section was removed; Quittio::fake() picks format and profile like a real run (defaults.format, automatic choice, profile keys, all formats with as()), honours htmlOnly() and throws for unknown formats or profiles.
  • English tax and legal texts of the German pack use the terms of Directive 2006/112/EC; LegalTexts notices take the preset texts, so one notice has one wording.
  • Font directories configured for the mPDF renderer take precedence over the bundled fonts and the mPDF fonts; fonts published with --tag=quittio-fonts are found without configuration.
  • New design of the default templates (Blade and plain PHP): logo or issuer name at the top left; the sender line at the top of the address field and the recipient directly below it (the DIN 5008 zones stay available to own templates); the invoice details on the right; the line table with a rule below the column heads, item names in medium weight, a VAT column only for several rates; totals with "USt. 19 %" for a single rate and the gross total (or the amount due) large in the primary colour; payment terms, bank details and payment QR code side by side; a footer in four columns (address; phone and e-mail; register and legal information; VAT identifier and tax number, which leave the invoice details) with the page number centred. New default colours #2741d9, #15171c and #5f6470, top margin of the first page 15 mm, fold and punch marks on by default (pdf.fold_marks), the bank details are no longer repeated in the footer, the account holder is printed only when it is not the issuer. German labels: "Beschreibung", "Betrag", "Zahlung", "GiroCode", "2 % Skonto bis 10.10.2026: 2.692,76 €", "Bitte bis zum 17.10.2026 überweisen." for credit transfers, "Ihre USt-IdNr."; service periods within one year read "01.10.–31.10.2026".

Fixed

  • Laravel: a published config/quittio.php without validation.levels keeps the default checks (php, xsd) instead of running none; only an empty list switches them off.
  • The hint of rule BR-CO-25 names ->dueInDays(14) instead of the callback form; a validation message with one finding reads "(1 error)".
  • Documentation: facts corrected against the code, among them the order in which the profile is chosen, the template of a presentation passed to ->presentation(), the signatures of line(), subLine() and the document line methods, the day count in the default interest examples, BundesbankBaseRates::fetch() (an instance method), the check of German tax numbers (DE-TAX-NUMBER) and that an unavailable veraPDF stops the rendering by default.
  • Amounts in locales with a multibyte grouping separator (French) were garbled: the digits were grouped by reversing the string bytes including the separator. ICU's narrow no-break space is now replaced by a no-break space, which every bundled font has.
  • Legal references: self-billed invoices are section 14 (2) sentence 5 UStG (was sentence 2); DE-USTG-10 names its basis (Article 230 VAT Directive, section 16 (6) UStG).
  • quittio:validate --kosit/--verapdf uses every driver setting (java, docker, timeout, fail_on_unavailable) and no longer changes the cached core; defaults.format = 'auto' is accepted in Laravel and as issuer default; quittio:preview and the preview route accept the views of pdf.templates; the facade docblock lists every manager and fake method; the ASCII fallback file name of downloads transliterates characters ("Ü" becomes "U").
  • Profile validation no longer warns about postal addresses in BASIC WL and BASIC; address fields carry their Factur-X levels and the business terms of their context (e.g. BT-55 for the buyer country). Data of EXTENDED and of the XRechnung extension, and fields below a field that is not allowed, are reported once.
  • Reading a CII or UBL invoice without document totals reports BR-12 to BR-15 instead of QUITTIO-NOT-CALCULATED.
  • In the view model of documents (quotes, reminders …) document.typeCode is null and document.type names the document type; the default templates highlight lines marked with highlight() and use colors.secondary; CountryDefaults::$paymentQr selects the QR code of the mode auto; self-billed invoices take their title from the country pack's correction policy.
  • paymentTerms() with a callable continues existing terms, also after InvoiceBuilder::from().
  • Country pack Germany: DE-USTG-06 accepts the pack's reverse charge texts in every shipped language; newVehicle() adds the note of section 14a (4) UStG; deductAdvance() writes the whole note in one language and rejects documents that cannot be advance invoices (credit notes, corrections, negative totals); VatPreset::exempt() defaults to German like the enum presets; DefaultInterest uses the day count of the dunning law (365) and BaseRateTable::periods() merges periods with the same rate.
  • Floats as amounts, prices, rates and quantities are rejected with an InvalidArgumentException also in files without declare(strict_types=1); before, PHP turned netPrice(19.99) into 19 for parameters typed BigDecimal|int|string. The same applies to InvoiceLine::withNetPrice()/withPriceInclVat(), CustomTaxPreset::make() and partial cancellations of the Managed layer.
  • UBL credit notes write the tender or lot reference (BT-17) as OriginatorDocumentReference in the element order of CreditNote; it was dropped before.
  • Default line identifiers skip identifiers set with id() (a line without id() and one with id('1') both got 1); identifiers used twice make build() throw InvalidInvoiceData, and data from other sources gets the warning QUITTIO-LINE-ID-DUPLICATE.
  • The configured profile (defaults.profile) applies to every format that allows it, also to ->format('zugferd') and formats chosen by a country rule; Config::$profile defaults to null (format or automatic choice decides). XRechnung CII with sub lines reports the sub lines in the validation instead of failing with "does not support profile extended".
  • The template of the presentation passed to ->presentation() and of the configured presentation selects the template (after ->template() and the invoice's own presentation).
  • Laravel: country packs listed in quittio.countries replace discovered packs with the same code (e.g. a subclass of CountryDE).
  • swiss-qr-bill without a generator fails with a ConfigurationException instead of printing no code; generators are registered with Quittio::withPaymentQrGenerator() or quittio.pdf.payment_qr.generator. PDF renderers whose supportsEmbedding() is false get PDF/A and the embedded XML from MpdfPostProcessor; CallbackRenderer and PassthroughRenderer report true.
  • A missing java, verapdf or docker binary is reported as KOSIT-UNAVAILABLE/VERAPDF-UNAVAILABLE instead of aborting the render run with an ErrorException in Laravel.
  • Country pack Germany: invoices with VatPreset::ZeroRatedPv (section 12 (3) UStG) failed BR-Z-10, because the preset sent an exemption reason; the zero rate is now stated as the rate only.
  • Country pack Germany: order numbers shaped like a Leitweg-ID (e.g. 4711-01) no longer switch the format to XRechnung; only a Leitweg-ID with valid check digits or a 0204 endpoint does, and the check digit warning is limited to public buyers.
  • Country pack Germany: "Gutschrift" is required for all self-billed type codes (261, 389, 527), not only 389.
  • Country pack Germany: default interest texts named the first day of the next period as end date; they now name the last day with interest.
  • Country pack Germany: the small amount simplifications (section 33 UStDV) no longer apply to reverse charge, intra-community supplies and distance sales (section 33 sentence 3 UStDV, rule DE-USTG-12); such invoices need all mandatory information and are not rendered as plain PDF.
  • Country pack Germany: DE-USTG-09 also rejects VAT on document level charges of small businesses; DE-USTG-01 requires the postcode of German buyers; DE-USTG-11 also applies to reverse charge lines without a preset when the buyer is in another member state.
  • Country pack Germany: ->deductAdvance() rejects advance invoices in another currency and deducts the amount received (including a cash rounding BT-114).
  • Country pack Germany: the base rate table rejects dates from the next scheduled change (1 January or 1 July) after the last known rate instead of silently using the old rate (BaseRateTable::validUntil()); the § 288 (5) BGB flat rate uses the money context of the documents.
  • quittio.storage.disk is used as the default disk of save().
  • Package dependencies: quittio/core requires ext-gd (needed by mPDF) and ext-ctype, quittio/country-de declares Brick, quittio/laravel and quittio/managed require laravel/framework (they use Illuminate\Foundation); the autoload prefix of quittio/laravel is Quittio\Laravel\.
  • Calculation: exemption reasons are only written into VAT breakdowns of categories that allow them (E, AE, K, G, O); S and Z breakdowns no longer fail BR-S-10/BR-Z-10.
  • Calculation: gross prices and document allowances are distributed with signed weights, so vouchers, returns and negative lines keep their sign and correct net amounts; a negative gross price is rejected.
  • Calculation: document allowances and charges accept an exemption reason and code for VAT groups they create alone; sub lines with a VAT category or rate other than their parent are rejected (QUITTIO-SUBLINE-VAT).
  • XML: BT-6 is omitted when it equals the invoice currency (BR-53 in CII); MINIMUM writes only the seller country; a warning reports preceding invoices (BG-3) that CII cannot carry (QUITTIO-PRECEDING-INVOICES-CII); XRechnung BT-20 contains free text, due days and late fee terms before the #SKONTO# lines.
  • Reading: exemption reasons of the VAT breakdown are copied to the lines, a declared BT-111 survives recalculation, all VATEX codes of the code list are accepted.
  • PDF: watermarked drafts and previews never embed e-invoice XML (an embedded format with a watermark throws), watermarked documents and HTML show the watermark.
  • PDF: the mandatory field guard only counts printed values (not count() or loops) and checks regulatory notes in legalNotes; the German pack uses the keys servicePeriod, line periods and taxRepresentative.vatId.
  • PDF: the seller's tax representative (BG-11), payee (BG-10), delivery address (BG-13) and price base quantity (BT-149) are printed; every cash discount tier shows its own amount; credit notes show "Erstattungsbetrag" without bank details; titles follow the type code family (self-billed → "Gutschrift", credit notes → "Rechnungskorrektur", 325 → "Proforma-Rechnung", 386 → "Anzahlungsrechnung"); commercial credit notes are no longer labelled "Gutschrift" in the code lists.
  • PDF: the DIN 5008 address field keeps its position with a logo; VAT rows are labelled per category; split allowances name their VAT rate; quotes and order confirmations show no payment block or GiroCode; the EPC payload respects the 331 byte limit; the render locale wins over the invoice locale.
  • Security: template names from requests or options are never file paths (PhpTemplateEngine resolves names only inside the configured template directories); the preview route accepts only the default, the configured and the package templates.
  • PDF/A: logos are re-encoded as RGB PNG on white (logos with transparency or above 600×200 px made every ZUGFeRD render fail), SVG logos are rasterised (Imagick) or rejected; the GiroCode PNG is compressed.
  • PDF: the mandatory field guard checks every line (name, quantity), every VAT row, every legal note, the cash discount terms, both buyer name and address, the service date (BT-72/BG-14, otherwise every line period) and the full tax representative; null values no longer count as printed.
  • PDF: self-billed invoices show the buyer as issuer and the seller as recipient, with payout wording and without GiroCode; margin scheme and travel agency rows state no VAT amount (section 14a (6) UStG); descriptions and notes are plain text by default (Markdown opt-in, it may change the displayed text); a watermark together with any XML format throws; per-call presentations merge over the configured one; payment reminders encode the total and the open invoice numbers in the GiroCode; DIN 5008: the recipient starts in the Anschriftzone.
  • XMP: fx:Version is the XRechnung version for the XRECHNUNG conformance level, the XRechnung extension declares XRECHNUNG.
  • XML (EXTENDED): exemption reasons on line and allowance/charge level, one payment terms group per cash discount tier, the exchange rate; German BT-20 free text uses German number and date formats.
  • Builder: payment() replaces the payment instructions instead of accumulating accounts (duplicate IBAN and lost BT-83 in foreign currency invoices, BR-DE-25-b on direct debit); percentage allowances and charges follow their base after line changes; InvoiceLine::withPriceInclVat(), price modes cannot be mixed.
  • Cancellations: read foreign currency invoices keep BT-111 (implied exchange rate), XRechnung third-party payments are reversed in full cancellations only, gross-price lines can be cancelled by amount, self-billed documents stay self-billed (389 ↔ 261).
  • Laravel: seller.identifiers (BT-29) for sellers without VAT identifier; explicit pdf.paper wins over the country default; unknown letter layouts and QR types throw; queued renders travel as snapshots through the upcaster chain; removed the unused defaults.currency, defaults.timezone and defaults.standard.
  • Reading: own PDF parser with cross-reference streams, object streams and incremental updates (PDFs from PDFBox, iText, Acrobat or Word could not be read); name tree loops, exponent numbers, oversized tags and attribute floods are rejected; strict dates (never replaced by today, BR-03 for a missing issue date); no default currency (BR-05), BT-110 only in the invoice currency, BT-149 from the gross price, BR-CL-06 for BT-8, BR-DEC as errors, sanitised attachment names, embedded files chosen via /AF with findings for ambiguous PDFs, a specific message for ZUGFeRD 1.0; MINIMUM and BASIC WL documents without lines no longer trigger line based rules.
  • Reading API: read() and validateFile() take the content only, never a path; readFile() and validatePath() read files.
  • Documents: proforma invoices (325) are rejected in the invoice pipeline (QUITTIO-PROFORMA, use DocumentBuilder::proforma()); payment reminders and account statements take no lines, the reminder total is open items + fees + interest without VAT and the GiroCode uses it with the open invoice numbers; account statements respect their period; interest uses actual/actual per calendar year and the text names the last interest day; level 1 reminders are titled "1. Mahnung".
  • Countries: the buyer's country pack also applies for sellers outside the EU (e.g. XRechnung for German public buyers); VatTerritory knows the EU VAT territory (XI for goods, Monaco, Isle of Man, Büsingen, Helgoland and other excluded areas); VIES is only queried for EU member states and XI, and unavailable answers are not treated as invalid; L7 warnings for intra-community supplies and exports to the wrong destination.
  • Partial cancellations reverse taxable amounts and VAT exactly: per VAT group the VAT of a cancellation is round2(cumulative cancelled taxable amount × rate) minus the VAT already cancelled, the closing cancellation takes the original's VAT; in gross price mode the net amounts are aligned the same way (cumulative gross / (1 + rate)), so BT-116 and BT-117 per group and BT-109 to BT-115 add up exactly over every sequence; each document stays within one cent of its own rounding and keeps the sign of its taxable amount (BR-CO-17); the same for BT-111 in foreign currency invoices; originals of other systems with their own rounding are aligned with their effective rate. Managed stores the cancelled amounts per group (vat on invoice relations), recomputes them under lock and refuses a cancellation that raced with another one; core users can pass a CancellationHistory, which is validated against the original. Aligned documents carry the info QUITTIO-CANCELLATION-VAT-ALIGNED and a note sentence when a group's VAT differs from its own rounding; drafts keep a declared VAT breakdown only for aligned cancellations.
  • Reading PDFs: the reconstruction of damaged files scans one object at a time and stops above 100,000 objects, PNG predictors only process the data present, the catalogue search no longer caches every object (memory and CPU exhaustion with small crafted files).
  • PDF: invoices with line groups (->group()) failed to render under Laravel (mPDF warning with packTableData); refunds and self-billed invoices no longer announce a direct debit; SVG logos are read only with the SVG coder at a bounded resolution, logos above 10 MB or 25 megapixels are rejected; the GiroCode beneficiary falls back to the seller name; partials cannot be previewed as templates.
  • Reading XML: encodings that hide the markup from the pre-scan (UTF-7, EBCDIC, UTF-32) and DOCTYPEs are rejected before libxml parses them, namespace declarations in scope are limited (CPU exhaustion with libxml < 2.12); BT-6 is no longer interpolated into XPath (injection of BT-111); relative date strings ("tomorrow 2026", "+2000 days") are rejected also in the builder; cash discount periods in weeks; bidi control characters are stripped from attachment names.
  • Calculation: different exemption reasons in one VAT group are joined in BT-120 (e.g. § 4 Nr. 14 UStG together with a margin scheme), BT-121 is only kept when the codes agree (QUITTIO-EXEMPTION-CONFLICT warning); percentage document allowances follow removed lines; line ids no longer collide after removeLine() and line().
  • Cancellations of self-billed invoices are titled "Storno-Gutschrift" with the note "Stornierung der Gutschrift …" (CorrectionPolicy::cancellationTitle(), title key cancellation_self_billed).
  • Country pack Germany: the § 3c distance sale exclusion of small amount invoices uses the VAT territory and the deliver-to address and only warns; "Gutschrift" may be given in another official EU language (UStAE 14.5 Abs. 24); small amount invoices in a foreign currency are compared by their EUR equivalent.
  • PDF: remote logos are fetched by Quittio only from public addresses (checked per resolved IP and redirect, connected to the checked IP), mPDF never fetches URLs; Blade templates are restricted to an allowlist (quittio.pdf.templates, default invoices.*); the guard requires the unit and the seller's legal name; refunds of negative invoices are shown as positive amounts; Presentation::$explicit allows per-invoice resets.
  • Cancellations and unchanged InvoiceBuilder::from() keep the declared line and VAT amounts of read invoices within the EN 16931 rounding tolerance, so foreign invoices using other rounding are reversed exactly (QUITTIO-DECLARED-AMOUNTS warns when amounts had to be recalculated); an exchange rate implied from BT-111 is dropped once amounts change; amount discounts split across VAT rates are re-split after line changes; third-party payments (BG-DEX-09) are rejected where the syntax cannot carry them (QUITTIO-THIRD-PARTY-PAYMENTS).
  • Validation knows the XML syntax that is written, so syntax specific rules apply (e.g. BR-DEX-15 for XRechnung CII); UBL writes BT-82 and the credit note due date only once with several accounts (UBL-SR-45/46).
  • PDF: mPDF cannot fetch anything over HTTP (deny-all client), logo values are strictly classified (no backslashes, protocol-relative URLs or surrounding whitespace; data URIs strictly decoded and re-encoded; SVG entity declarations refused in every mode); IPv6 site-local addresses are refused for remote logos.
  • Declared amounts: only set when the recalculation does not reproduce them, line net amounts kept only within 0.02 of the unrounded formula (QUITTIO-LINE-ROUNDING), and only while a fingerprint of the amount inputs matches (also for read data edited before from()), so edits outside the builder never keep stale amounts; categories without VAT never keep a declared VAT amount; PEPPOL-EN16931-R120 uses the official tolerance of 0.02.
  • Country pack Germany: DE-XRECHNUNG-EXT-01 rejects credit notes of the XRechnung extension in UBL (no validator scenario; use the negative_invoice strategy).
  • pdf.paper and the VIES verification (validation.drivers.vies.enabled) take effect; file names of XML-only formats no longer contain a raw translation key.
  • Watermarks (draft and preview PDFs) were never printed: mPDF needs showWatermarkText, and PDF/A forbids their transparency. Watermarked PDFs without embedded XML are now rendered without PDF/A.