g-efac / laravel
Integracion de Laravel para el SDK de G-eFac (facturacion electronica e-CF)
Requires
- php: >=8.2
- g-efac/sdk: ^0.2
- guzzlehttp/guzzle: ^7.9
- guzzlehttp/psr7: ^2.7
- illuminate/contracts: ^12.0
- illuminate/support: ^12.0
Requires (Dev)
- laravel/pint: ^1.18
- orchestra/testbench: ^10.0
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.5
README
Puente de Laravel para g-efac/sdk: registra EfacClient en el contenedor, expone la
fachada Efac y traduce la configuración de Laravel (.env, caché) al SDK. No agrega
API propia — es cableado.
¿Vas a implementar la emisión? Este README es la instalación. La Guía de integración es el manual completo: qué se envía, qué se recibe, idempotencia, estados, errores, reintentos, pruebas y una implementación de referencia de punta a punta.
Estabilidad: versión 0.x
/v1 es provisional — todavía no está congelada. Mientras siga así, este paquete se
publica en 0.x y las versiones menores pueden romper compatibilidad: no hay promesa
de semver hasta que la API se congele. Cuando se active el disparador de congelación, el
paquete pasa a 1.0.0 y adopta semver en serio. El detalle completo del disparador y del
compromiso de compatibilidad está documentado en el README de g-efac/sdk.
Instalación
composer require g-efac/laravel
Requiere Laravel 12. Laravel 11 no está soportado: cada release 11.x depende de correcciones de seguridad que solo existen en 12.x, así que instalar este paquete sobre Laravel 11 dejaría la aplicación expuesta a advisories ya resueltos en la versión siguiente.
Gracias al auto-discovery de Composer, instalar el paquete ya registra el service
provider y el alias Efac. No hay que tocar config/app.php ni ningún otro archivo de
arranque.
Configuración
Publica el archivo de configuración:
php artisan vendor:publish --tag=efac-config
Esto crea config/efac.php. Las siete opciones, todas configurables por variable de
entorno:
| Variable | Config key | Valor por defecto | Significado |
|---|---|---|---|
EFAC_BASE_URL |
efac.base_url |
https://api.g-efac.com |
URL base de la API |
EFAC_CLIENT_ID |
efac.client_id |
— (obligatoria) | Client id OAuth2 del emisor |
EFAC_CLIENT_SECRET |
efac.secret |
— (obligatoria) | Client secret OAuth2 |
EFAC_SCOPE |
efac.scope |
efac.v1 |
Scope OAuth2 solicitado |
EFAC_CACHE_STORE |
efac.cache_store |
null (store por defecto) |
Store de caché de Laravel donde se guarda el access token |
EFAC_CONNECT_TIMEOUT |
efac.connect_timeout |
5 |
Timeout de conexión de Guzzle, en segundos |
EFAC_TIMEOUT |
efac.timeout |
30 |
Timeout de la petición completa de Guzzle, en segundos |
Solo EFAC_CLIENT_ID y EFAC_CLIENT_SECRET no tienen valor por defecto. Si alguna falta
o queda en blanco, el paquete lanza una RuntimeException clara — pero no durante el
boot() del framework, sino la primera vez que se resuelve EfacClient del
contenedor. Esto es deliberado: un fallo duro en el arranque rompería artisan en
cualquier entorno sin credenciales configuradas — CI, un clone recién hecho, un
contenedor corriendo migraciones — donde de todas formas nadie está llamando a la API.
Uso
Con la fachada Efac
use Efac\Laravel\Efac; use Efac\Sdk\Enum\EcfTipo; use Efac\Sdk\Enum\TaxTreatment; use Efac\Sdk\Invoice\Invoice; use Efac\Sdk\Invoice\Item; $factura = Invoice::of(EcfTipo::CreditoFiscal31) ->seller('101000001', 'ACME SRL', 'Av. Principal 1') ->buyer('130000002', 'Cliente SA', email: 'cxp@cliente.do') ->addItem(Item::goods('Widget', qty: 2, unitPrice: 500.00, tax: TaxTreatment::Itbis18)) ->build(); $resultado = Efac::invoices()->emit($factura, 'orden-1042'); echo $resultado->encf; // E310000000001 echo $resultado->qrUrl; // URL del timbre para la Representación Impresa
Con inyección de dependencias
EfacClient está registrado como singleton, así que también se puede pedir por tipo en
cualquier sitio que resuelva el contenedor — un controlador, un job, un comando:
namespace App\Http\Controllers; use Efac\Sdk\EfacClient; use Efac\Sdk\Enum\EcfTipo; use Efac\Sdk\Enum\TaxTreatment; use Efac\Sdk\Invoice\Invoice; use Efac\Sdk\Invoice\Item; final class EmitirFacturaController { public function __invoke(EfacClient $efac) { $factura = Invoice::of(EcfTipo::CreditoFiscal31) ->seller('101000001', 'ACME SRL', 'Av. Principal 1') ->buyer('130000002', 'Cliente SA', email: 'cxp@cliente.do') ->addItem(Item::goods('Widget', qty: 2, unitPrice: 500.00, tax: TaxTreatment::Itbis18)) ->build(); $resultado = $efac->invoices()->emit($factura, 'orden-1042'); return response()->json(['encf' => $resultado->encf]); } }
La fachada y la inyección por tipo llegan al mismo EfacClient singleton — no hay
diferencia de comportamiento entre una y otra, solo de estilo.
La llave de idempotencia es obligatoria
emit() exige una llave, tanto vía la fachada como vía inyección — el paquete no la
genera por ti. La razón es fiscal, no estética: sin Idempotency-Key, reintentar una
petición que expiró por timeout puede emitir un e-CF duplicado y consumir un segundo
e-NCF, que es un recurso fiscal con seguimiento legal. La explicación completa, junto con
las reglas de reintento y de reuso de la llave, está en el README de g-efac/sdk.
El resto
Este paquete no duplica documentación del SDK. EfacClient y Invoices, que se ven en los
ejemplos de arriba, son g-efac/sdk; lo único que aporta g-efac/laravel es la fachada, el
service provider y la traducción de config/efac.php a EfacClient::builder().
| Qué buscas | Dónde |
|---|---|
| Implementar la emisión en una app Laravel, de principio a fin | Guía de integración |
| Referencia del SDK sin Laravel | README de g-efac/sdk |
Contrato HTTP /v1 |
docs/api/openapi-v1.json |
La guía cubre en detalle lo que este README solo menciona: el cuerpo JSON exacto que se
envía, cada campo de las respuestas, las reglas de idempotencia, la tabla de estados, el
manejo de 202 con awaitTerminal() o con jobs reprogramados, la matriz de reintentos por
excepción, la caché del token, el ambiente DGII, multi-emisor y cómo hacer pruebas.
Qué no incluye este paquete
Deliberadamente no hay comandos Artisan, jobs en cola ni modelos Eloquent. El bridge se
limita a poner un EfacClient correctamente configurado en el contenedor; cómo lo llamas
— sincrónico, desde un job propio, desde un comando propio — queda en tu aplicación.