Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Verifactu Laravel Package

squareetlabs/laravel-verifactu

Laravel package for Veri*factu-compliant invoicing. It helps generate invoice records, QR codes, hashes and XML, and integrates Spanish anti-fraud invoicing requirements into Eloquent models and Laravel apps.

View on GitHub
Deep Wiki
Context7

Getting Started

What this package is (and isn't)

squareetlabs/laravel-verifactu is a Laravel wrapper around Spain's VeriFactu invoicing regime (RD 1007/2023) — the anti-fraud obligation that requires your billing software to produce tamper-evident, hash-chained invoice records, print a QR on every invoice, and be able to ship those records to the AEAT. The package handles the plumbing (record schema, chaining/hashing, QR payload, submission endpoints), not your business logic. You still own invoice numbering, VAT calculation, and your domain model.

Treat it as compliance infrastructure: it must not be the thing that surprises you in production. Read the README's config table and the migration files before you write a single line of integration code.

Minimal setup

composer require squareetlabs/laravel-verifactu
php artisan vendor:publish --provider="SquareetLabs\Verifactu\VerifactuServiceProvider"
php artisan migrate

Then populate config/verifactu.php (or the published .env keys) with at minimum:

  • Environment / endpoint: AEAT has a pruebas (test) and producción endpoint. Never start on production.
  • Issuer identity: NIF, nombre/razón social — these must match your certificado exactly or AEAT rejects signatures.
  • Certificate: path + password for the client certificate used to sign submissions (typically a .p12/.pfx). Store the password in config sourced from an env var, never committed.
  • Software system identity (IdSistemaInformatico, configurable since v1.0.2): the code that identifies your billing system inside every record header. Prior to 1.0.2 this was hardcoded, which meant every project using the package presented the same identity to AEAT — workable for a single production deployment, useless the moment you need two test instances. If you are on 1.0.1 or earlier, upgrade before you go near the pruebas environment.
  • Hash/QR parameters: algorithm version, "VeriFactu" vs "No VeriFactu" mode flag.

Where to look first

  1. config/verifactu.php — every behavioural switch lives here. Skim it fully; it's the fastest map of the package's capabilities. On 1.0.2+ confirm IdSistemaInformatico is actually present in your published copy (see Gotchas).
  2. database/migrations/ — the record table shape tells you exactly what fields you must supply. This is the real contract.
  3. The service class (typically VerifactuService or similar) — the public methods are your integration surface.
  4. src/Models/ and any Concerns/ or Traits/ directory — check whether a HasVerifactu-style trait is meant to be dropped onto your existing Invoice model.
  5. Tests / fixtures in the repo — cheapest source of a working end-to-end example.

First use case: register one invoice in the sandbox

The smallest meaningful loop is: create an invoice → register it (record + hash + QR) → retrieve the QR string → submit to the test endpoint. Do that in a seeder or a tinker session, not in a controller. Something along these lines (adapt method names to the actual README — API naming is the one thing you should verify rather than assume):

$invoice = Invoice::create([...]);          // your model, your numbering
$record  = Verifactu::register($invoice);   // package creates the chained record
$qr      = $record->qrPayload();            // embed in PDF / HTML
$response = Verifactu::send($record);       // hits AEAT test endpoint

Once that round-trips cleanly, you have a working mental model and can design the real integration.


Implementation Patterns

Pattern 1 — Wrap it behind your own invoice service

Do not let controllers call the package directly. Introduce a thin domain service (e.g. App\Services\Fiscal\InvoiceRegistrar) that:

  1. Persists your invoice in a DB transaction.
  2. Calls the package to build and persist the VeriFactu record inside the same transaction.
  3. Emits an application event (InvoiceFiscallyRegistered).
  4. Dispatches submission to a queue.

This keeps the package swappable (there are competing VeriFactu packages) and gives you one place to add idempotency keys, logging, and retries.

DB::transaction(function () use ($invoice) {
    $invoice->save();
    $record = Verifactu::register($invoice);
    $invoice->update(['verifactu_uuid' => $record->uuid]);
});
SubmitVerifactuRecord::dispatch($invoice)->onQueue('fiscal');

Pattern 2 — Queue everything that talks to AEAT

AEAT calls are slow, rate-limited, and can time out. Always dispatch them. Recommended job shape:

  • $tries = 5, backoff() returning [60, 300, 900, 3600, 7200].
  • ShouldBeUnique keyed on the record UUID, so a duplicate dispatch can't double-submit.
  • On terminal failure, write to a verifactu_submission_failures table and alert — never silently swallow.
  • Distinguish transport failures (retry) from AEAT validation rejections (do not retry — a malformed record will fail forever until corrected; route to a human queue).

Pattern 3 — Chaining and immutability

VeriFactu is a hash chain: each record's hash incorporates the previous one. That has three consequences for your architecture:

  • Serialise registration. Use a DB lock (->lockForUpdate() on the last record, or a dedicated sequence row) around record creation. Concurrent registrations racing on the chain head will produce a broken chain that you cannot retroactively fix.
  • No hard deletes, no edits. A corrective invoice is a new record of type "rectificativa" pointing at the original. Enforce this at the model level ($table->softDeletes() plus a boot guard that throws on delete()).
  • Store the full snapshot. Persist the exact payload you hashed. If you later change invoice fields, the stored hash must still verify byte-for-byte.

Pattern 4 — QR generation as a view concern

The QR is not decoration; its payload has a mandated structure. Centralise it in a Blade component or a QrData value object so PDF, HTML email, and customer portal all render identical content:

<x-verifactu-qr :invoice="$invoice" />

Generate the QR at issuance time and persist the string on the record — do not regenerate it lazily from the live model later, or a later edit (e.g. changing the customer name) will silently change the QR and desynchronise it from the filed record.

Pattern 5 — Multi-tenant / multi-company

If you issue invoices for more than one legal entity, config-level issuer settings are the wrong place. Check whether the package supports per-record issuer overrides or a "company profile" object; if not, instantiate the service with runtime config via a factory bound in the container per company:

$this->app->bind(Verifactu::class, fn () => Verifactu::forCompany($tenant->fiscalProfile));

This is a common gap in 20-star packages — verify it early, before you build thirty features on top. v1.0.2 partially closes this gap: IdSistemaInformatico is now configurable, so multiple installs/instances can present distinct software-system identities (which the pruebas environment requires if you run more than one sandbox). That is identity-per-deployment, not identity-per-tenant — it does not give you per-company issuer/NIF overrides. If you need true multi-entity support you are still building the factory yourself.

Pattern 6 — Testing strategy

  • Unit: freeze the hash and QR output against known fixtures. If a package upgrade changes hash format, you want a red test, not an AEAT rejection in week 3.
  • Feature: use Laravel's Http::fake() to stub AEAT responses (both accept-with-errors and reject cases) and assert your failure-handling paths.
  • Reconciliation: a nightly verifactu:verify-chain style Artisan command that re-walks the chain is the single highest-value thing you can build on top of this package. See below.
  • Test-instance isolation: if you run parallel CI jobs or staging + local sandboxes against AEAT pruebas, give each a distinct IdSistemaInformatico (or NumeroInstalacion if the package exposes it). Two jobs sharing one identity produce confusing duplicate/collision responses that look like bugs in your code.

Gotchas and Tips

Config changes on upgrade — re-publish, don't blind-overwrite

IdSistemaInformatico moving from hardcoded to config-driven is a config-file change, not a schema change, so composer update alone will not surface it: your published config/verifactu.php from 1.0.1 has no key for it, and you'll silently keep whatever the package falls back to. After upgrading to 1.0.2:

  1. Diff the package's config/verifactu.php against your published copy (php artisan vendor:publish --tag=verifactu-config --force in a scratch checkout, then git diff).
  2. Merge only the new keys. --force publishing directly in your repo will clobber every environment-specific value you set.
  3. Add the key to your .env / .env.example and to your deployment secrets checklist, not just the config file.

Also read the actual PR diffs for v1.0.2 (#7 and #10) rather than trusting the release notes — "Feat/integration pr8 issue6" tells you nothing about whether a public method signature moved.

Compliance is not the package's job — it's yours

The package can generate a correctly-hashed record for a factually wrong invoice. It cannot know that your VAT was computed wrong, that the invoice date predates the registro, or that the numbering series skipped. Budget for a reconciliation layer:

// Verifies: contiguous numbering, hash chain integrity, submission status
php artisan fiscal:audit --from=2026-01-01

Ship this before go-live, not after. AEAT's plazo for correction (4 years, with 4-year retention obligations) means errors you don't detect are errors you live with.

Certificate handling is the usual failure point

  • AEAT returns cryptic errors for expired or wrong-chain certificates. Proactively alert on certificate expiry (openssl_x509_parse in a scheduled job).
  • Many Spanish providers issue the signing certificate as .p12 with a password that contains characters your .env parser will mangle — quote it.
  • In Docker/queued contexts, ensure the certificate path resolves from the container's filesystem, not the host's. storage_path()-based paths are the safe choice.
  • Test and production certificates are different. Make it structurally impossible to use production certs from a local env: assert app()->environment('production') in the config when the endpoint is production.

Identity fields matter as much as signatures

IdSistemaInformatico (and the sibling NombreSistemaInformatico / Version / NumeroInstalacion fields where present) are part of the signed record payload. Changing them mid-year means records with two different software identities in one chain — an audit question you do not want to answer. Treat the value as immutable once you have filed real invoices: set it deliberately at go-live, document it, and only change it with a migration plan. For non-production, vary it freely — that is exactly what 1.0.2 unlocks.

Date and clock issues

VeriFactu records carry a generation timestamp. A container with a drifting clock produces timestamps that can look out of order relative to the chain, or fall on the wrong side of a submission deadline. Keep NTP synced and consider using a single now() source rather than mixing Carbon::now() and DB CURRENT_TIMESTAMP (different timezones → different values → audit confusion). Store UTC in DB, present in Europe/Madrid.

"No VeriFactu" mode exists — know why you'd (not) use it

The regulation allows systems to declare themselves as not VeriFactu-compliant, with a different QR annotation. This is a trap if chosen casually: it's a permanent visible label on your invoices. Only use it if you genuinely are not required to be compliant (e.g. non-Spanish scope). Verify what the package's mode flag actually does to the QR payload and the sent record.

Submission responses are not binary

AEAT returns a mix of correct, correct-with-errors, and incorrect states, and duplicate submissions return a different code than you might expect. Map these into an enum in your app rather than checking booleans:

enum SubmissionStatus: string {
    case Accepted = 'Correcto';
    case AcceptedWithErrors = 'AceptadoConErrores';
    case Rejected = 'Incorrecto';
    case Duplicate = 'Duplicado';
}

Treat AcceptedWithErrors as needing review, not as success.

Extension points worth knowing

  • Events: most packages in this space fire events on registration/submission — subscribe to them for your audit log rather than polluting the service layer with Log::info.
  • Publishable views/stubs: if the QR view is publishable, publish and own it; you'll want to restyle it for your PDF template.
  • Model swapping: check for a config('verifactu.models.record') style key. If it exists, subclass the record model early and add your own scopes/accessors there rather than wrapping it externally.
  • Format: check whether the package emits AEAT's XML directly or via a Facturae dependency. If it's XML-over-HTTP, note that AEAT's schema validators are strict about element ordering — never hand-edit the XML.

Version pinning

Laravel 10–12 compatibility is a wide net, and a package with ~23 stars likely ships breaking changes without a major-version bump discipline you'd expect from a larger vendor. v1.0.2 is a case in point: a patch bump added a config key and (per the vague second PR title) some integration changes, with release notes that don't enumerate the surface area touched. Pin to a specific minor/patch version, run the hash-fixture tests on every upgrade, and diff config/verifactu.php plus src/ on each bump — not just composer.json.

Debugging checklist when a submission fails

  1. Is the certificate valid and matching the NIF? (Most common cause.)
  2. Is the endpoint right for the environment?
  3. Is IdSistemaInformatico set, and does it match what you registered with AEAT for that environment? (New in 1.0.2 — a missing/duplicated identity is now a failure mode you can create yourself.)
  4. Does the record validate locally? — check whether the package exposes a validate()/dry-run before sending.
  5. Is the chain head correct? — a corrupted chain fails on every subsequent record, so a "sudden flood of errors" usually means one bad record upstream.
  6. Has your model changed since registration? — recomputing a hash from mutated fields will never match.
  7. Did the job double-submit? — check for Duplicado responses and missing uniqueness constraints on your job.

One structural recommendation

Because this package's correctness is measured in years (retention + audit windows), invest early in an append-only local mirror of every record plus its submission response, exported to cold storage on a schedule. If the package's schema ever changes under you, or you migrate off it, that mirror is what lets you prove compliance — and it's the single cheapest insurance you can buy against a small, single-maintainer dependency sitting on your critical path.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core