Paquete Laravel 10/11/12 para gestión y registro de facturación electrónica VeriFactu
- Modelos Eloquent para invoices, breakdowns y recipients
- Enum types para campos fiscales (invoice type, tax type, regime, etc.)
- Helpers para operaciones de fecha, string y hash
- Servicio AEAT client (configurable e inyectable)
- Form Requests para validación
- API Resources para respuestas RESTful
- Factories y tests unitarios para todos los componentes core
- Modos duales: VERIFACTU y NO VERIFACTU (Requerimiento)
- Helper QR: Generación automática de URLs para códigos QR según modo y entorno
- Campos avanzados: Encadenamiento blockchain, facturas rectificativas, subsanación
- Multi-tenant: Soporte para múltiples instalaciones bajo el mismo NIF
- Clientes extranjeros: Soporte para identificadores internacionales
- Estado AEAT: Campos para tracking de respuestas y estado de registro
- Listo para extensión y uso en producción
composer require squareetlabs/laravel-verifactuPublica la configuración y migraciones:
php artisan vendor:publish --provider="Squareetlabs\VeriFactu\Providers\VeriFactuServiceProvider"
php artisan migrateEdita tu archivo .env o config/verifactu.php según tus necesidades:
return [
'enabled' => true,
'system_id' => env('VERIFACTU_SYSTEM_ID', '01'),
'default_currency' => 'EUR',
'issuer' => [
'name' => env('VERIFACTU_ISSUER_NAME', ''),
'vat' => env('VERIFACTU_ISSUER_VAT', ''),
],
// Modo Verifactu (true) o NO Verifactu/Requerimiento (false)
'verifactu_mode' => env('VERIFACTU_MODE', true),
// Parámetros del Sistema Informático
'tipo_uso_posible_solo_verifactu' => env('VERIFACTU_TIPO_USO_SOLO_VF', 'N'),
'tipo_uso_posible_multi_ot' => env('VERIFACTU_TIPO_USO_MULTI_OT', 'S'),
'indicador_multiples_ot' => env('VERIFACTU_INDICADOR_MULTI_OT', 'N'),
// Define si se cargan las migraciones del paquete (por defecto false)
'load_migrations' => env('VERIFACTU_LOAD_MIGRATIONS', false),
];Este paquete soporta dos modos de facturación:
El modo estándar de facturación electrónica.
VERIFACTU_MODE=truePara facturas bajo requerimiento de la Agencia Tributaria. Requiere un RefRequerimiento proporcionado por Hacienda.
VERIFACTU_MODE=falseEl paquete ajustará automáticamente las URLs del servicio SOAP según el modo configurado:
- Producción VERIFACTU:
https://www1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP - Producción NO VERIFACTU:
https://www1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/RequerimientoSOAP - Pruebas VERIFACTU:
https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP - Pruebas NO VERIFACTU:
https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/RequerimientoSOAP
Configura los parámetros según las características de tu sistema:
- tipo_uso_posible_solo_verifactu: Indica si el sistema solo puede usarse en modo VERIFACTU (
S) o también en modo NO VERIFACTU (N) - tipo_uso_posible_multi_ot: Indica si el sistema puede usarse con múltiples obligados tributarios (
SoN) - indicador_multiples_ot: Indica si existen múltiples obligados tributarios en el sistema (
SoN)
VERIFACTU_TIPO_USO_SOLO_VF=N
VERIFACTU_TIPO_USO_MULTI_OT=S
VERIFACTU_INDICADOR_MULTI_OT=NTodas las opciones de configuración pueden establecerse mediante variables de entorno en tu archivo .env:
# Configuración básica
VERIFACTU_ISSUER_NAME="Mi Empresa S.L."
VERIFACTU_ISSUER_VAT="B12345678"
VERIFACTU_SYSTEM_ID="01"
VERIFACTU_DEFAULT_CURRENCY="EUR"
# Modo de facturación
VERIFACTU_MODE=true # true = VERIFACTU, false = NO VERIFACTU
# Parámetros del Sistema Informático
VERIFACTU_TIPO_USO_SOLO_VF=N # S = Solo VERIFACTU, N = Ambos modos
VERIFACTU_TIPO_USO_MULTI_OT=S # S = Múltiples OT, N = Un solo OT
VERIFACTU_INDICADOR_MULTI_OT=N # S = Existen múltiples OT, N = Un solo OT
# Migraciones
VERIFACTU_LOAD_MIGRATIONS=false # true = Cargar migraciones del paqueteEl archivo config/verifactu.php incluye la siguiente estructura:
return [
'enabled' => env('VERIFACTU_ENABLED', true),
'system_id' => env('VERIFACTU_SYSTEM_ID', '01'),
'default_currency' => env('VERIFACTU_DEFAULT_CURRENCY', 'EUR'),
'issuer' => [
'name' => env('VERIFACTU_ISSUER_NAME', ''),
'vat' => env('VERIFACTU_ISSUER_VAT', ''),
],
'verifactu_mode' => env('VERIFACTU_MODE', true),
'sistema_informatico' => [
'tipo_uso_posible_solo_verifactu' => env('VERIFACTU_TIPO_USO_SOLO_VF', 'N'),
'tipo_uso_posible_multi_ot' => env('VERIFACTU_TIPO_USO_MULTI_OT', 'S'),
'indicador_multiples_ot' => env('VERIFACTU_INDICADOR_MULTI_OT', 'N'),
],
'load_migrations' => env('VERIFACTU_LOAD_MIGRATIONS', false),
];Este paquete soporta dos modos de uso:
Si estás empezando un proyecto desde cero, puedes usar los modelos y migraciones que incluye el paquete.
- Habilita las migraciones en tu
.env:VERIFACTU_LOAD_MIGRATIONS=true
- Ejecuta las migraciones:
php artisan migrate
- Usa los modelos
Squareetlabs\VeriFactu\Models\Invoice,BreakdownyRecipientdirectamente.
Si ya tienes tu propio sistema de facturación, no necesitas usar nuestras migraciones. Solo necesitas implementar los contratos en tus modelos.
- Asegúrate de que
VERIFACTU_LOAD_MIGRATIONSesfalse(por defecto). - Genera un adaptador para tu modelo de factura:
php artisan verifactu:make-adapter Invoice
- Esto generará el código necesario en tu terminal. Cópialo a tu modelo
App\Models\Invoicee implementa la interfazVeriFactuInvoice.
Ejemplo de implementación:
use Squareetlabs\VeriFactu\Contracts\VeriFactuInvoice;
use Illuminate\Database\Eloquent\Model;
class Invoice extends Model implements VeriFactuInvoice
{
// Implementa los métodos requeridos por la interfaz
public function getInvoiceNumber(): string
{
return $this->invoice_number; // Tu campo personalizado
}
// ... resto de métodos
}use Squareetlabs\VeriFactu\Http\Requests\StoreInvoiceRequest;
use Squareetlabs\VeriFactu\Models\Invoice;
use Squareetlabs\VeriFactu\Http\Resources\InvoiceResource;
public function store(StoreInvoiceRequest $request)
{
$invoice = Invoice::create($request->validated());
// Opcionalmente puedes asociar breakdowns y recipients
// $invoice->breakdowns()->createMany([...]);
// $invoice->recipients()->createMany([...]);
return new InvoiceResource($invoice->load(['breakdowns', 'recipients']));
}El paquete soporta todos los tipos de factura según la normativa AEAT:
| Tipo | Enum | Descripción |
|---|---|---|
| F1 | InvoiceType::STANDARD |
Factura completa |
| F2 | InvoiceType::SIMPLIFIED |
Factura simplificada |
| F3 | InvoiceType::SUBSTITUTE |
Factura que sustituye a otra |
| F4 | InvoiceType::EXPORT |
Factura de exportación |
| R1 | InvoiceType::RECTIFICATIVE_R1 |
Rectificativa por sustitución o diferencia |
| R2 | InvoiceType::RECTIFICATIVE_R2 |
Rectificativa por anulación |
| R3 | InvoiceType::RECTIFICATIVE_R3 |
Rectificativa por descuento |
| R4 | InvoiceType::RECTIFICATIVE_R4 |
Rectificativa por incremento |
| R5 | InvoiceType::RECTIFICATIVE_R5 |
Rectificativa por otros conceptos |
A continuación, ejemplos de cómo crear cada tipo de invoice usando el modelo y enums:
use Squareetlabs\VeriFactu\Models\Invoice;
use Squareetlabs\VeriFactu\Enums\InvoiceType;
$invoice = Invoice::create([
'number' => 'INV-STD-001',
'date' => '2024-07-01',
'customer_name' => 'Standard Customer',
'customer_tax_id' => 'C12345678',
'issuer_name' => 'Issuer S.A.',
'issuer_tax_id' => 'B87654321',
'amount' => 100.00,
'tax' => 21.00,
'total' => 121.00,
'type' => InvoiceType::STANDARD,
]);$invoice = Invoice::create([
'number' => 'INV-SIMP-001',
'date' => '2024-07-01',
'customer_name' => 'Simplified Customer',
'customer_tax_id' => 'C87654321',
'issuer_name' => 'Issuer S.A.',
'issuer_tax_id' => 'B87654321',
'amount' => 50.00,
'tax' => 10.50,
'total' => 60.50,
'type' => InvoiceType::SIMPLIFIED,
]);$invoice = Invoice::create([
'number' => 'INV-SUB-001',
'date' => '2024-07-01',
'customer_name' => 'Substitute Customer',
'customer_tax_id' => 'C11223344',
'issuer_name' => 'Issuer S.A.',
'issuer_tax_id' => 'B87654321',
'amount' => 80.00,
'tax' => 16.80,
'total' => 96.80,
'type' => InvoiceType::SUBSTITUTE,
// Puedes añadir aquí la relación con facturas sustituidas si implementas la lógica
]);use Squareetlabs\VeriFactu\Enums\RectificativeType;
$invoice = Invoice::create([
'number' => 'INV-RECT-001',
'date' => '2024-07-01',
'customer_name' => 'Rectified Customer',
'customer_tax_id' => 'C55667788',
'issuer_name' => 'Issuer S.A.',
'issuer_tax_id' => 'B87654321',
'amount' => 120.00,
'tax' => 25.20,
'total' => 145.20,
'type' => InvoiceType::RECTIFICATIVE_R1,
'rectificative_type' => 'S', // S=Sustitución, I=Diferencia
'rectified_invoices' => json_encode(['INV-001', 'INV-002']),
'rectification_amount' => json_encode(['base' => -50.00, 'tax' => -10.50, 'total' => -60.50]),
]);$invoice = Invoice::create([
'number' => 'INV-EXP-001',
'date' => '2024-07-01',
'customer_name' => 'Foreign Customer Ltd',
'customer_tax_id' => null,
'customer_country' => 'GB',
'issuer_name' => 'Issuer S.A.',
'issuer_tax_id' => 'B87654321',
'amount' => 500.00,
'tax' => 0.00, // Exportaciones exentas de IVA
'total' => 500.00,
'type' => InvoiceType::EXPORT,
]);// Primera factura de la cadena
$firstInvoice = Invoice::create([
'number' => 'INV-001',
'date' => '2024-07-01',
'is_first_invoice' => true,
'amount' => 100.00,
'tax' => 21.00,
'total' => 121.00,
// ... otros campos
]);
// Segunda factura enlazada
$secondInvoice = Invoice::create([
'number' => 'INV-002',
'date' => '2024-07-02',
'is_first_invoice' => false,
'previous_invoice_number' => 'INV-001',
'previous_invoice_date' => '2024-07-01',
'previous_invoice_hash' => $firstInvoice->hash,
'amount' => 150.00,
'tax' => 31.50,
'total' => 181.50,
// ... otros campos
]);$invoice = Invoice::create([
'number' => 'INV-SUB-001',
'date' => '2024-07-01',
'is_subsanacion' => true,
'rejected_invoice_number' => 'INV-REJECTED-001',
'rejection_date' => '2024-06-30',
'amount' => 100.00,
'tax' => 21.00,
'total' => 121.00,
// ... otros campos
]);Nota: Para facturas rectificativas y sustitutivas, si implementas los campos y relaciones adicionales (como facturas rectificadas/sustituidas, tipo de rectificación, importe de rectificación), deberás añadirlos en el array de creación.
El modelo Invoice incluye campos avanzados para cumplir con todas las especificaciones AEAT:
$invoice = Invoice::create([
'number' => 'INV-002',
'is_first_invoice' => false, // false si no es la primera
'previous_invoice_number' => 'INV-001', // Número de factura anterior
'previous_invoice_date' => '2024-07-01', // Fecha de factura anterior
'previous_invoice_hash' => 'ABC123...', // Hash de factura anterior
// ... otros campos
]);$invoice = Invoice::create([
'number' => 'INV-RECT-001',
'type' => InvoiceType::RECTIFICATIVE_R1,
'rectificative_type' => 'I', // 'S' = Sustitución, 'I' = Diferencia
'rectified_invoices' => [ // Array de facturas rectificadas
'INV-001',
'INV-002'
],
'rectification_amount' => [ // Importes de rectificación
'base' => -50.00,
'tax' => -10.50,
'total' => -60.50
],
// ... otros campos
]);$invoice = Invoice::create([
'number' => 'INV-SUBS-001',
'is_subsanacion' => true, // Indica que es subsanación
'rejected_invoice_number' => 'INV-REJECTED-001', // Número de factura rechazada
'rejection_date' => '2024-06-30', // Fecha de rechazo
// ... otros campos
]);Si tu sistema gestiona múltiples instalaciones bajo el mismo NIF emisor:
$invoice = Invoice::create([
'number' => 'INV-001',
'numero_instalacion' => 'INST-001', // Identificador de instalación
// ... otros campos
]);Después de enviar una factura a AEAT, puedes actualizar estos campos:
$invoice->update([
'aeat_status' => 'ACEPTADA', // Estado de respuesta AEAT
'aeat_response_code' => '0', // Código de respuesta
'aeat_response_message' => 'Registro aceptado', // Mensaje de respuesta
'aeat_registration_date' => now(), // Fecha de registro en AEAT
'aeat_csv' => 'CSV-123456789', // CSV de registro
'has_aeat_warnings' => false, // Si tiene advertencias
]);$invoice = Invoice::create([
'number' => 'INV-001',
'operation_date' => '2024-07-01', // Fecha de operación (si difiere de date)
'tax_period' => '01', // Período fiscal (01, 02, 0A, etc.)
'external_reference' => 'REF-EXT-123', // Referencia externa opcional
'description' => 'Descripción de la operación',
'status' => 'draft', // Estado interno: draft, sent, accepted, rejected
// ... otros campos
]);| Campo | Tipo | Descripción |
|---|---|---|
number |
string | Número/serie de factura |
date |
date | Fecha de expedición |
type |
InvoiceType enum | Tipo de factura (F1, F2, R1, etc.) |
amount |
decimal | Base imponible |
tax |
decimal | Importe de impuestos |
total |
decimal | Importe total |
| Campo | Tipo | Descripción |
|---|---|---|
customer_name |
string|null | Nombre del cliente |
customer_tax_id |
string|null | NIF/CIF del cliente |
customer_country |
string|null | País del cliente (ISO) |
issuer_name |
string | Nombre del emisor |
issuer_tax_id |
string | NIF/CIF del emisor |
issuer_country |
string | País del emisor (ISO) |
| Campo | Tipo | Descripción |
|---|---|---|
is_first_invoice |
boolean | Si es la primera factura de la cadena |
previous_invoice_number |
string|null | Número de factura anterior |
previous_invoice_date |
date|null | Fecha de factura anterior |
previous_invoice_hash |
string|null | Hash de factura anterior |
hash |
string|null | Hash de esta factura |
| Campo | Tipo | Descripción |
|---|---|---|
rectificative_type |
string|null | 'S' = Sustitución, 'I' = Diferencia |
rectified_invoices |
array | Array de números de facturas rectificadas |
rectification_amount |
array | Importes de rectificación (base, tax, total) |
| Campo | Tipo | Descripción |
|---|---|---|
is_subsanacion |
boolean | Si es una subsanación |
rejected_invoice_number |
string|null | Número de factura rechazada |
rejection_date |
date|null | Fecha de rechazo |
| Campo | Tipo | Descripción |
|---|---|---|
numero_instalacion |
string|null | Identificador de instalación |
| Campo | Tipo | Descripción |
|---|---|---|
aeat_status |
string|null | Estado: ACEPTADA, RECHAZADA, ENVIADA |
aeat_response_code |
string|null | Código de respuesta AEAT |
aeat_response_message |
string|null | Mensaje de respuesta |
aeat_registration_date |
datetime|null | Fecha de registro en AEAT |
aeat_csv |
string|null | CSV de registro |
has_aeat_warnings |
boolean | Si tiene advertencias |
| Campo | Tipo | Descripción |
|---|---|---|
operation_date |
date|null | Fecha de operación (si difiere de date) |
tax_period |
string|null | Período fiscal (01, 02, 0A, etc.) |
correction_type |
string|null | Tipo de corrección |
external_reference |
string|null | Referencia externa |
description |
string|null | Descripción de la operación |
status |
string | Estado interno (draft, sent, accepted, rejected) |
csv |
string|null | CSV interno |
Para clientes extranjeros, usa el campo id_type en el modelo Recipient:
use Squareetlabs\VeriFactu\Models\Invoice;
use Squareetlabs\VeriFactu\Models\Recipient;
use Squareetlabs\VeriFactu\Enums\ForeignIdType;
$invoice = Invoice::create([
'number' => 'INV-INT-001',
'date' => '2024-07-01',
'customer_name' => 'Foreign Customer Ltd',
'customer_tax_id' => null, // Sin NIF para extranjeros
'customer_country' => 'GB', // Código ISO del país
// ... otros campos
]);
// Crear recipient con identificador extranjero
$recipient = Recipient::create([
'invoice_id' => $invoice->id,
'name' => 'Foreign Customer Ltd',
'tax_id' => 'GB123456789', // VAT number del país
'country' => 'GB',
'id_type' => ForeignIdType::VAT_NUMBER, // Tipo de identificador
]);Tipos de identificadores extranjeros disponibles:
ForeignIdType::VAT_NUMBER- Número de IVAForeignIdType::PASSPORT- PasaporteForeignIdType::OFFICIAL_ID- Documento oficial de identidadForeignIdType::RESIDENCE_CERTIFICATE- Certificado de residenciaForeignIdType::OTHER- Otro tipo
use Squareetlabs\VeriFactu\Models\Invoice;
use Squareetlabs\VeriFactu\Enums\InvoiceType;
// Factura original
$originalInvoice = Invoice::create([
'number' => 'INV-001',
'date' => '2024-07-01',
'amount' => 100.00,
'tax' => 21.00,
'total' => 121.00,
'type' => InvoiceType::STANDARD,
// ... otros campos
]);
// Factura rectificativa por diferencia
$rectificativeInvoice = Invoice::create([
'number' => 'INV-RECT-001',
'date' => '2024-07-15',
'type' => InvoiceType::RECTIFICATIVE_R1,
'rectificative_type' => 'I', // I = Diferencia
'rectified_invoices' => ['INV-001'], // Facturas rectificadas
'rectification_amount' => [
'base' => -20.00, // Diferencia en base
'tax' => -4.20, // Diferencia en impuesto
'total' => -24.20 // Diferencia total
],
'amount' => -20.00, // Importe negativo
'tax' => -4.20,
'total' => -24.20,
// ... otros campos
]);$invoice = Invoice::create([
'number' => 'INV-001',
'date' => '2024-07-01',
'numero_instalacion' => 'INST-001', // Identificador de instalación
'issuer_name' => 'Empresa Principal S.A.',
'issuer_tax_id' => 'B12345678',
// ... otros campos
]);
// Otra factura de diferente instalación pero mismo emisor
$invoice2 = Invoice::create([
'number' => 'INV-002',
'date' => '2024-07-01',
'numero_instalacion' => 'INST-002', // Diferente instalación
'issuer_name' => 'Empresa Principal S.A.',
'issuer_tax_id' => 'B12345678', // Mismo NIF emisor
// ... otros campos
]);// Primera factura de la cadena
$first = Invoice::create([
'number' => 'INV-001',
'date' => '2024-07-01',
'is_first_invoice' => true,
'amount' => 100.00,
'tax' => 21.00,
'total' => 121.00,
// ... otros campos
]);
// Calcular hash (se hace automáticamente si está configurado)
$first->hash = HashHelper::generateInvoiceHash([...])['hash'];
$first->save();
// Segunda factura enlazada
$second = Invoice::create([
'number' => 'INV-002',
'date' => '2024-07-02',
'is_first_invoice' => false,
'previous_invoice_number' => $first->number,
'previous_invoice_date' => $first->date,
'previous_invoice_hash' => $first->hash,
'amount' => 150.00,
'tax' => 31.50,
'total' => 181.50,
// ... otros campos
]);
// Tercera factura enlazada
$third = Invoice::create([
'number' => 'INV-003',
'date' => '2024-07-03',
'is_first_invoice' => false,
'previous_invoice_number' => $second->number,
'previous_invoice_date' => $second->date,
'previous_invoice_hash' => $second->hash,
'amount' => 200.00,
'tax' => 42.00,
'total' => 242.00,
// ... otros campos
]);// Factura original que fue rechazada
$rejectedInvoice = Invoice::create([
'number' => 'INV-REJECTED-001',
'date' => '2024-06-30',
'aeat_status' => 'RECHAZADA',
'aeat_response_code' => '1',
'aeat_response_message' => 'Error en validación',
// ... otros campos
]);
// Factura de subsanación
$subsanacionInvoice = Invoice::create([
'number' => 'INV-SUBS-001',
'date' => '2024-07-01',
'is_subsanacion' => true,
'rejected_invoice_number' => 'INV-REJECTED-001',
'rejection_date' => '2024-06-30',
// Corregir los datos que causaron el rechazo
'amount' => 100.00, // Corregido
'tax' => 21.00,
'total' => 121.00,
// ... otros campos corregidos
]);use Illuminate\Http\Request;
use Squareetlabs\VeriFactu\Services\AeatClient;
use Squareetlabs\VeriFactu\Models\Invoice;
class InvoiceAeatController extends Controller
{
public function send(Request $request, AeatClient $aeatClient, $invoiceId)
{
$invoice = Invoice::with(['breakdowns', 'recipients'])->findOrFail($invoiceId);
$result = $aeatClient->sendInvoice($invoice);
// Actualizar campos de estado si el envío fue exitoso
if ($result['status'] === 'success') {
$invoice->update([
'aeat_status' => $result['aeat_status'] ?? 'ENVIADA',
'aeat_response_code' => $result['code'] ?? null,
'aeat_response_message' => $result['message'] ?? null,
'aeat_registration_date' => $result['registration_date'] ?? null,
'aeat_csv' => $result['csv'] ?? null,
'has_aeat_warnings' => $result['has_warnings'] ?? false,
]);
}
return response()->json($result, $result['status'] === 'success' ? 200 : 422);
}
}Nota: Protege este endpoint con autenticación/autorización adecuada.
El resultado incluirá el XML enviado y recibido, útil para depuración.
Si el certificado no es válido o hay error de validación, el array tendrá 'status' => 'error' y 'message'.
Puedes consultar el estado de las facturas después de enviarlas:
use Squareetlabs\VeriFactu\Models\Invoice;
// Buscar facturas por estado
$acceptedInvoices = Invoice::where('aeat_status', 'ACEPTADA')->get();
$rejectedInvoices = Invoice::where('aeat_status', 'RECHAZADA')->get();
$pendingInvoices = Invoice::whereNull('aeat_status')->get();
// Buscar facturas con advertencias
$invoicesWithWarnings = Invoice::where('has_aeat_warnings', true)->get();
// Buscar facturas por CSV
$invoice = Invoice::where('aeat_csv', 'CSV-123456789')->first();
// Obtener información completa de una factura
$invoice = Invoice::find(1);
echo "Estado: " . $invoice->aeat_status;
echo "Código: " . $invoice->aeat_response_code;
echo "Mensaje: " . $invoice->aeat_response_message;
echo "CSV: " . $invoice->aeat_csv;
echo "Fecha registro: " . $invoice->aeat_registration_date;use Squareetlabs\VeriFactu\Http\Requests\StoreBreakdownRequest;
use Squareetlabs\VeriFactu\Models\Breakdown;
public function storeBreakdown(StoreBreakdownRequest $request)
{
$breakdown = Breakdown::create($request->validated());
return response()->json($breakdown);
}use Squareetlabs\VeriFactu\Helpers\DateTimeHelper;
use Squareetlabs\VeriFactu\Helpers\StringHelper;
use Squareetlabs\VeriFactu\Helpers\HashHelper;
$dateIso = DateTimeHelper::formatIso8601('2024-01-01 12:00:00');
$sanitized = StringHelper::sanitize(' &Hello <World> ');
$hash = HashHelper::generateInvoiceHash([
'issuer_tax_id' => 'A12345678',
'invoice_number' => 'INV-001',
'issue_date' => '2024-01-01',
'invoice_type' => 'F1',
'total_tax' => '21.00',
'total_amount' => '121.00',
'previous_hash' => '',
'generated_at' => '2024-01-01T12:00:00+01:00',
]);El paquete incluye un helper para generar las URLs necesarias para los códigos QR que deben incluirse en las facturas según la normativa VERIFACTU.
use Squareetlabs\VeriFactu\Helpers\QrUrlHelper;
use Squareetlabs\VeriFactu\Models\Invoice;
// Obtener la factura
$invoice = Invoice::find(1);
// Generar URL para QR en modo VERIFACTU (producción)
$qrUrl = QrUrlHelper::build(
$invoice,
'B12345678', // NIF del emisor
true, // true = producción, false = pruebas
true // true = VERIFACTU, false = NO VERIFACTU
);
// Resultado: https://www2.agenciatributaria.gob.es/wlpl/TIKE-CONT/ValidarQR?nif=B12345678&numserie=FAC-001&fecha=15-01-2024&importe=121.00El helper generará automáticamente la URL correcta según el entorno y modo:
- Producción VERIFACTU:
https://www2.agenciatributaria.gob.es/wlpl/TIKE-CONT/ValidarQR - Producción NO VERIFACTU:
https://www2.agenciatributaria.gob.es/wlpl/TIKE-CONT/ValidarQRNoVerifactu - Pruebas VERIFACTU:
https://prewww2.aeat.es/wlpl/TIKE-CONT/ValidarQR - Pruebas NO VERIFACTU:
https://prewww2.aeat.es/wlpl/TIKE-CONT/ValidarQRNoVerifactu
Si no proporcionas el parámetro verifactuMode, el helper usará el valor configurado en config/verifactu.php:
// Usa la configuración por defecto del modo
$qrUrl = QrUrlHelper::build($invoice, 'B12345678', true);Ejemplo en un controlador:
use Squareetlabs\VeriFactu\Helpers\QrUrlHelper;
use Squareetlabs\VeriFactu\Models\Invoice;
class InvoiceQrController extends Controller
{
public function generateQr($invoiceId)
{
$invoice = Invoice::findOrFail($invoiceId);
$issuerVat = config('verifactu.issuer.vat');
$isProduction = app()->environment('production');
$qrUrl = QrUrlHelper::build($invoice, $issuerVat, $isProduction);
// Generar el código QR usando alguna librería (ej: SimpleSoftwareIO/simple-qrcode)
// $qrCode = QrCode::size(300)->generate($qrUrl);
return response()->json([
'url' => $qrUrl,
// 'qr_image' => $qrCode
]);
}
}Puedes disparar eventos cuando se crean, actualizan o envían invoices a AEAT. Ejemplo:
// app/Events/InvoiceSentToAeat.php
namespace App\Events;
use Squareetlabs\VeriFactu\Models\Invoice;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class InvoiceSentToAeat
{
use Dispatchable, SerializesModels;
public function __construct(public Invoice $invoice, public array $aeatResponse) {}
}Despacha el evento tras el envío:
use App\Events\InvoiceSentToAeat;
// ... después de enviar a AEAT
InvoiceSentToAeat::dispatch($invoice, $result);Crea un listener para notificaciones o logging:
// app/Listeners/LogAeatResponse.php
namespace App\Listeners;
use App\Events\InvoiceSentToAeat;
use Illuminate\Support\Facades\Log;
class LogAeatResponse
{
public function handle(InvoiceSentToAeat $event)
{
Log::info('AEAT response', [
'invoice_id' => $event->invoice->id,
'response' => $event->aeatResponse,
]);
}
}Registra tu evento y listener en EventServiceProvider:
protected $listen = [
\App\Events\InvoiceSentToAeat::class => [
\App\Listeners\LogAeatResponse::class,
],
];Puedes restringir el acceso a invoices usando policies de Laravel:
// app/Policies/InvoicePolicy.php
namespace App\Policies;
use App\Models\User;
use Squareetlabs\VeriFactu\Models\Invoice;
class InvoicePolicy
{
public function view(User $user, Invoice $invoice): bool
{
return $user->id === $invoice->user_id;
}
public function update(User $user, Invoice $invoice): bool
{
return $user->id === $invoice->user_id && $invoice->status === 'draft';
}
}Registra la policy en AuthServiceProvider:
protected $policies = [
\Squareetlabs\VeriFactu\Models\Invoice::class => \App\Policies\InvoicePolicy::class,
];Úsala en tu controller:
public function update(Request $request, Invoice $invoice)
{
$this->authorize('update', $invoice);
// ...
}Puedes notificar a usuarios o admins cuando una invoice se envía o falla:
// app/Notifications/InvoiceSentNotification.php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Squareetlabs\VeriFactu\Models\Invoice;
class InvoiceSentNotification extends Notification
{
use Queueable;
public function __construct(public Invoice $invoice) {}
public function via($notifiable)
{
return ['mail'];
}
public function toMail($notifiable)
{
return (new \Illuminate\Notifications\Messages\MailMessage)
->subject('Invoice Sent to AEAT')
->line('Invoice #' . $this->invoice->number . ' was sent to AEAT successfully.');
}
}Despacha la notificación en tu job o listener:
$user->notify(new \App\Notifications\InvoiceSentNotification($invoice));Puedes enviar invoices a AEAT de forma asíncrona usando colas:
use Squareetlabs\VeriFactu\Models\Invoice;
use App\Jobs\SendInvoiceToAeatJob;
// Despacha el job a la cola
SendInvoiceToAeatJob::dispatch($invoice->id);En tu job, implementa ShouldQueue:
use Illuminate\Contracts\Queue\ShouldQueue;
class SendInvoiceToAeatJob implements ShouldQueue
{
// ...
}Configura tu conexión de cola en .env y ejecuta el worker:
php artisan queue:workPuedes usar paquetes como owen-it/laravel-auditing para auditar cambios en invoices:
- Instala el paquete:
composer require owen-it/laravel-auditing
- Añade el contrato
\OwenIt\Auditing\Contracts\Auditablea tu modelo:use OwenIt\Auditing\Contracts\Auditable; class Invoice extends Model implements Auditable { use \OwenIt\Auditing\Auditable; // ... }
- Ahora todos los cambios en invoices serán auditados automáticamente. Puedes ver los logs:
$audits = $invoice->audits;
Ejecuta todos los tests unitarios:
php artisan test
# o
vendor/bin/phpunitLas contribuciones son bienvenidas. Por favor:
- Fork el proyecto
- Crea una rama para tu feature
- Commit tus cambios
- Push a la rama
- Abre un Pull Request
Este paquete es open-source bajo la Licencia MIT.
- Documentación técnica: https://sede.agenciatributaria.gob.es/Sede/iva/sistemas-informaticos-facturacion-verifactu/informacion-tecnica.html
- Issues: https://github.com/squareetlabs/LaravelVerifactu/issues
- Alberto Rial Barreiro - SquareetLabs
- Jacobo Cantorna Cigarrán - SquareetLabs
Si este paquete te ha sido útil, ¡no olvides darle una estrella en GitHub!