josemmo/verifactu-php
Josemmo Verifactu PHP is a framework-agnostic library for Spain’s VERI*FACTU e-invoicing rules, helping generate QR codes and XML, sign invoices, validate records, and submit them to AEAT. It simplifies compliance for PHP and Laravel applications.
josemmo/Verifactu-PHP is a pure-PHP implementation of Spain's Verifactu invoice-record submission flow (XML generation, hash chain, QR, SOAP to AEAT). v0.3.4 is still a 0.x release, so treat the API as moving and pin the exact version in composer.json.
Minimal setup:
composer require josemmo/verifactu-php:0.3.4ext-soap, ext-openssl, ext-dom. AEAT communication is SOAP + signed XML, so a missing soap extension is the usual first failure..pfx/.p12 plus passphrase) and the issuing NIF/NombreRazon into a config object. There is no "driver" abstraction here — the certificate is the identity of the emitter, not of the app.entorno: test / preproducción endpoint). Never develop against production.b3e98a9, 92f3809) — start there rather than at the source, because the guides cover the end-to-end flow (alta → huella → QR → envío → respuesta) that is tedious to reconstruct from class names alone.First use case to implement: emit a single registro de alta for a simplified invoice (FacturaSimplificada), compute the huella (chained hash), render the QR, and submit it. Get that round-trip green in test before you touch the chain, cancellations, or requerimientos — the chain logic is where the real complexity lives and you want a known-good single record to compare against.
Model the registro, don't pass invoices around. Convert your domain invoice into the package's factura/registro object in one dedicated mapper class. Every future change from AEAT (new fields, new record types) lands in that mapper instead of leaking into controllers and jobs.
Persist before you transmit. Store the generated XML, the huella, the timestamp, the environment, and the AEAT response (CSV, EstadoEnvio, EstadoRegistro, error list) in your own table before calling AEAT. AEAT is the source of truth for acceptance, but your database has to be the source of truth for what you sent and when. Audits (and the 4-year retention rule) depend on it.
Treat the hash chain as a database concern. The huella of invoice n depends on the previous record's huella for the same NIF + environment + record type. In practice:
verifactu_chain table keyed by (nif, entorno, tipo_registro) holding the last hash.lockForUpdate().Queue the submissions. A SubmitInvoiceRecord job with retries and exponential backoff (backoff(), retryUntil()), plus a distinct terminal-failure job for records AEAT rejects on content. Distinguish transport failures (retry) from validation failures (do not retry — resubmitting a rejected record just burns rate limit).
Handle the response states explicitly. AEAT returns both an overall send status and a per-record status; a 200 SOAP response can still contain Incorrecto. Map both into your own enum and drive UI/status from that, not from "did the call throw".
Use the new requerimiento support as a first-class flow, not an exception path. v0.3.4 adds support for requerimientos de información (8eec20b1): AEAT can ask you to submit records you previously sent voluntarily or not at all. Pattern:
Keep this in its own service. Mixing "normal day-to-day submission" and "respond to an inspection request" in the same method makes both untestable.
Distinguish voluntary submission from legal obligation. Verifactu records are normally sent voluntarily (remisión voluntaria) as you issue invoices. The new baja de remisión voluntaria (e6ac51a7) lets you cancel such a submission. Note the vocabulary in your code: Anulacion (record-level cancellation of an invoice), Baja (cancellation of a submission), Sustitucion (correcting a record). They are not interchangeable and using the wrong one is not a recoverable mistake at AEAT.
Use factories/fixtures for tests. Build registros from fixtures and assert on generated XML + huella against a small set of golden files. The chain algorithm is deterministic — pin its output in tests so a package upgrade that changes serialization shows up as a test failure rather than a production rejection.
Expose an Artisan command for reconciliation. verifactu:reconcile that pulls pending/unconfirmed records and re-queries their state. Cheap to write, saves hours when a queue worker dies mid-send.
"josemmo/verifactu-php": "0.3.4" (not ^0.3) and read the changelog diff of each release; minor bumps have added record types and altered method signatures before. There is no deprecation policy yet to rely on.openssl CLI can still fail in PHP. Also: certificates expire. Add a check on boot or in a health command; an expired cert surfaces as an opaque SOAP fault.FechaHoraHusoGenRegistro must be Europe/Madrid with correct DST offset. Don't let config('app.timezone') leak into record generation; compute explicitly and store UTC separately.Baja record is itself a chained record with its own huella, so your cancellation flow must go through the exact same chain-locking path as an Alta. Anywhere you write a record, use the same code path.__getLastRequest()/__getLastResponse()) and log both at debug alongside the huella. Nine out of ten rejections are visible in the XML in under a minute; the tenth is the hash chain, which you debug by replaying fixtures through your own chain table.How can I help you explore Laravel packages today?