ecf-dgii
SDK de Ruby para la API ECF DGII — Comprobantes Fiscales Electrónicos de la República Dominicana.
Índice
- Instalación
- Inicio rápido
- Configuración
- EcfClient — Backend (Full Access)
- EcfFrontendClient — Frontend (Read-Only)
- Polling con backoff exponencial
- Manejo de errores
- Arquitectura Backend / Frontend
- Licencia
Instalación
Añade esta línea al Gemfile de tu aplicación:
gem 'ecf-dgii'
Y luego ejecuta:
bundle install
O instálalo tú mismo con:
gem install ecf-dgii
Inicio rápido
require 'ecf-dgii'
# 1. Configurar el cliente
client = EcfDgii::Client.new(api_key: "tu-token-jwt", environment: :test)
# 2. Construir el objeto eCF (en este caso un Ecf31ECF)
ecf = EcfDgii::Generated::Ecf31ECF.new(
encabezado: EcfDgii::Generated::Ecf31Encabezado.new(
version: "Version1_0",
id_doc: EcfDgii::Generated::Ecf31IdDoc.new(
tipoe_cf: "FacturaDeCreditoFiscalElectronica",
encf: "E310000051630",
tipo_pago: "Contado",
tipo_ingresos: "01",
tabla_formas_pago: [
EcfDgii::Generated::Ecf31FormaDePago.new(
forma_pago: "Efectivo",
monto_pago: 1015.25
)
],
indicador_monto_gravado: "ConITBISIncluido",
fecha_vencimiento_secuencia: "2028-12-31T00:00:00"
),
emisor: EcfDgii::Generated::Ecf31Emisor.new(
rnc_emisor: "131460941",
razon_social_emisor: "DOCUMENTOS ELECTRONICOS DE 02",
direccion_emisor: "AVE. ISABEL AGUIAR NO. 269, ZONA INDUSTRIAL DE HERRERA",
fecha_emision: "2026-01-10"
),
comprador: EcfDgii::Generated::Ecf31Comprador.new(
rnc_comprador: "131880681",
razon_social_comprador: "DOCUMENTOS ELECTRONICOS DE 03"
),
totales: EcfDgii::Generated::Ecf31Totales.new(
itbis1: 18,
monto_gravado_i1: 762.71,
monto_gravado_total: 762.71,
total_itbis1: 137.29,
total_itbis: 137.29,
monto_no_facturable: 100.0,
impuestos_adicionales: [
EcfDgii::Generated::Ecf31ImpuestoAdicional2.new(
tipo_impuesto: "002",
tasa_impuesto_adicional: 2,
otros_impuestos_adicionales: 15.25
)
],
monto_impuesto_adicional: 15.25,
monto_total: 1015.25,
monto_periodo: 1015.25
)
),
detalles_items: [
EcfDgii::Generated::Ecf31Item.new(
numero_linea: 1,
nombre_item: "Iphone 18 Pro max",
indicador_facturacion: "ITBIS1_18Percent",
indicador_bieno_servicio: "Bien",
cantidad_item: 1,
unidad_medida: "Unidad",
precio_unitario_item: 1016.95,
monto_item: 1016.95,
tabla_impuesto_adicional: [
EcfDgii::Generated::Ecf31ImpuestoAdicional.new(tipo_impuesto: "002")
]
),
EcfDgii::Generated::Ecf31Item.new(
numero_linea: 2,
nombre_item: "Costo de Envío",
indicador_facturacion: "NoFacturable_18Percent",
indicador_bieno_servicio: "Servicio",
cantidad_item: 1,
unidad_medida: "Unidad",
precio_unitario_item: 100.0,
monto_item: 100.0
)
],
descuentos_o_recargos: [
EcfDgii::Generated::Ecf31DescuentoORecargo.new(
tipo_valor: "$",
tipo_ajuste: "D",
numero_linea: 1,
monto_descuentoo_recargo: 84.75,
descripcion_descuentoo_recargo: "Descuento",
indicador_facturacion_descuentoo_recargo: "ITBIS1_18Percent"
)
]
)
# 3. Enviar el eCF con enrutamiento automático y polling hasta completar
result = client.send_ecf(ecf)
puts "eCF Procesado: #{result.encf} - Estado: #{result.progress}"
Configuración
Autenticación
El API key (token JWT Bearer provisto por SSD) puede pasarse de forma directa o mediante variables de entorno:
# Parámetro directo
client = EcfDgii::Client.new(api_key: "tu-token-jwt")
# Variable de entorno ECF_API_KEY
# export ECF_API_KEY=tu-token-jwt
client = EcfDgii::Client.new
Ambientes de trabajo
client = EcfDgii::Client.new(environment: :test) # Pruebas (Predeterminado)
client = EcfDgii::Client.new(environment: :cert) # Certificación
client = EcfDgii::Client.new(environment: :prod) # Producción
# URL base personalizada (sobreescribe la del ambiente)
client = EcfDgii::Client.new(base_url: "https://mi-servidor.local/v1")
Integración en Ruby on Rails
El SDK incluye un generador para Rails que configura de forma global el cliente de facturación electrónica.
Ejecuta el generador en tu terminal:
bundle exec rails generate ecf_dgii:install
Esto creará el archivo de configuración config/initializers/ecf_dgii.rb con el siguiente contenido:
# config/initializers/ecf_dgii.rb
EcfDgii.configure do |config|
# Token JWT (Bearer) proveído por SSD
config.api_key = ENV["ECF_API_KEY"]
# Ambiente de trabajo: :test, :cert, o :prod (por defecto es :test)
config.environment = ENV.fetch("ECF_ENVIRONMENT", "test").to_sym
# URL base personalizada (opcional, sobreescribe el ambiente)
# config.base_url = ENV["ECF_API_URL"]
# Tiempo de espera máximo para solicitudes HTTP (por defecto es 30 segundos)
# config.timeout = 30
end
Una vez configurado el inicializador, puedes acceder al cliente en cualquier parte de tu aplicación de Rails:
client = EcfDgii.client
EcfClient — Backend (Full Access)
El EcfDgii::Client ofrece acceso completo a la API de ECF: envío de comprobantes, consultas, operaciones de empresa y certificados, consultas DGII, etc.
Envío de ECF con polling automático
El método send_ecf (1:1 con el sendEcf del SDK de TypeScript) maneja de forma transparente:
- Validación completa — verifica que
tipoeCF,rncEmisoryencfestén presentes (mismo comportamiento que TypeScript). - Enrutamiento dinámico — selecciona el endpoint correcto según el atributo
tipoe_cfde la cabecera. - Polling con backoff exponencial — consulta repetidamente el estado de procesamiento del eCF hasta llegar a un estado terminal (
FinishedoError). - EcfError en errores — si el progreso termina en
Error, lanzaEcfErrorcon la respuesta completa.
# Envío con polling automático (reemplaza a send_ecf_and_poll)
result = client.send_ecf(ecf)
# Con opciones de polling personalizadas
result = client.send_ecf(ecf, EcfDgii::PollingOptions.new(
initial_delay: 1.0,
max_retries: 60,
timeout: 120.0
))
Nota:
send_ecf_and_pollsigue disponible como alias por compatibilidad, pero ahorasend_ecfya incluye el polling (1:1 con TypeScript).
Operaciones de Empresa
# Listar empresas registradas
companies = client.get_companies(page: 1, limit: 10)
# Obtener los datos de una empresa específica por RNC
company = client.get_company_by_rnc("101001010")
# Registrar o actualizar datos de una empresa
req = EcfDgii::Generated::UpsertCompanyRequest.new(
rnc: "101001010",
legal_name: "Empresa de Pruebas SRL",
name: "Empresa de Pruebas"
)
client.upsert_company(req)
# Eliminar empresa
client.delete_company("101001010")
Certificados P12
# Obtener el certificado actual de la empresa
certificate = client.get_certificate("101001010")
# Subir/actualizar un certificado de firma digital P12
client.update_certificate("101001010", File.open("ruta/al/certificado.p12", "rb"), "clave-del-certificado")
Los métodos antiguos
get_current_certificateyupdate_certificate_companysiguen disponibles como alias.
Consultas DGII
# Consultar estado actual del eCF
estado = client.consulta_estado("101001010", "101001010", "E310000051630", "131880681", "ABC123")
# Consultar directorio de emisores electrónicos activos en DGII
directorio = client.consulta_directorio_listado("101001010")
# Consultar RFCE (incluye código de seguridad, 1:1 con TypeScript)
rfce = client.consulta_rfce("101001010", "101001010", "E310000051630", "SEC123")
# Consultar estado de procesamiento mediante el track_id obtenido
resultado = client.consulta_resultado("101001010", "track-id-obtenido")
# Obtener estatus general de los servicios de la DGII
estatus = client.estatus_servicios("101001010")
Recepción y aprobación comercial
# Buscar solicitudes de recepción
requests = client.search_ecf_reception_requests(page: 1, limit: 10)
# Obtener una solicitud de recepción por RNC y messageId
request = client.get_ecf_reception_request("101001010", "msg_123")
# Enviar aprobación comercial (ACECF) para un messageId
client.aprobacion_comercial("msg_123", body)
# Anular rangos de comprobantes
client.anulacion_rangos("101001010", anulacion_request)
# Firmar semilla
client.firmar_semilla("101001010", xml_body)
EcfFrontendClient — Frontend (Read-Only)
El EcfDgii::FrontendClient es un cliente restringido que solo expone endpoints GET (solo lectura), diseñado para usarse en frontends. El manejo del token es automático:
- En cada petición, verifica si hay un token en caché. Si no, obtiene uno nuevo y lo almacena.
- En respuestas
401, obtiene un token nuevo, actualiza la caché y reintenta la petición.
¡NUEVO! — Ahora disponible en Ruby (1:1 con EcfFrontendClient del SDK de TypeScript).
# Crear un frontend client
frontend = EcfDgii::FrontendClient.new(
get_token: -> { fetch_fresh_token_from_backend }, # Requerido
environment: :test
)
# Solo operaciones de lectura disponibles
ecfs = frontend.search_ecfs("131460941", page: 1, limit: 10)
company = frontend.get_company_by_rnc("131460941")
Factory method
frontend = EcfDgii.create_frontend_client(
get_token: -> { fetch_fresh_token_from_backend },
environment: :test
)
Cache personalizado
Por defecto el token se guarda en ~/.ecf-dgii/token. Puedes proveer tu propia lógica de caché:
frontend = EcfDgii::FrontendClient.new(
get_token: -> { fetch_fresh_token_from_backend },
cache_token: ->(token) { Redis.current.set("ecf-token", token) },
get_cached_token: -> { Redis.current.get("ecf-token") },
environment: :test
)
Polling con backoff exponencial
El SDK incluye un módulo de polling genérico que puedes usar directamente:
resultado = EcfDgii::Polling.poll_until_complete do
client.query_ecf(rnc, encf)
end
Opciones de polling (1:1 con TypeScript)
= EcfDgii::PollingOptions.new(
initial_delay: 1.0, # Segundos antes de la primera consulta (default: 1.0)
max_delay: 30.0, # Límite máximo de espera entre consultas (default: 30.0)
max_retries: 60, # Intentos máximos (default: 60, 0 = ilimitado)
backoff_multiplier: 2.0, # Multiplicador del retraso en cada intento (default: 2.0)
timeout: nil, # Timeout total en segundos (nil = sin timeout)
cancellation: -> { stop_polling_flag } # Callable de cancelación opcional
)
Valores terminales
El polling termina cuando el progreso es "Finished" (éxito) o "Error" (fallo), igual que el SDK de TypeScript y el contrato de la API.
Manejo de errores
El SDK utiliza una jerarquía de excepciones tipadas, 1:1 con el SDK de TypeScript:
begin
result = client.send_ecf(ecf)
rescue EcfDgii::EcfError => e
# Error de procesamiento del ECF — incluye la respuesta completa
puts "Error del ECF: #{e.}"
puts "Respuesta completa: #{e.response.inspect}" if e.response
rescue EcfDgii::PollingTimeoutError => e
puts "El polling tomó más tiempo de lo permitido: #{e.}"
rescue EcfDgii::PollingMaxRetriesError => e
puts "Se superó el número máximo de reintentos: #{e.}"
rescue EcfDgii::Generated::ApiError => e
puts "Error de API de ECF (Estatus: #{e.code})"
puts "Respuesta: #{e.response_body}"
rescue ArgumentError => e
puts "Error de validación: #{e.}"
rescue => e
puts "Ocurrió un error inesperado: #{e.}"
end
Jerarquía de errores
StandardError
└── EcfDgii::EcfError # Error base del SDK (incluye response)
├── EcfDgii::PollingTimeoutError # Timeout de polling
└── EcfDgii::PollingMaxRetriesError # Máximo de reintentos
EcfDgii::PollingErrorse mantiene como alias deEcfErrorpor compatibilidad.
Arquitectura Backend / Frontend
sequenceDiagram
participant C as Cliente (Browser/App)
participant BE as Backend (Rails)
participant ECF as ECF API
C->>BE: POST /invoice (datos de factura)
Note over BE: Valida, guarda y convierte a formato eCF
BE->>ECF: POST /ecf/{tipo} (enviar eCF)
ECF-->>BE: { messageId }
BE-->>C: { messageId }
Note over C: No espera — puede continuar
alt Token en cache
C->>C: Usar token existente
else Sin token o expirado
C->>BE: GET /ecf-token
BE->>ECF: POST /apikey (solo lectura, scoped a RNC)
ECF-->>BE: { apiKey }
BE-->>C: { apiKey }
C->>C: Almacenar token en cache
end
loop Polling hasta completar
C->>ECF: GET /ecf/{rnc}/{encf} (token solo lectura)
ECF-->>C: { progress, codSec, ... }
end
- Tu backend en Rails convierte las facturas internas a los modelos del eCF y los transmite con
client.send_ecf(ecf). - La API de ECF responde de inmediato con un
messageIdpara que el backend Rails responda al cliente web sin bloquearse. - El frontend usa
EcfDgii::FrontendClientcon un token de solo lectura (obtenido viaclient.create_api_key) para hacer polling directamente contra la API de eCF de forma segura.
Licencia
La gema está disponible como software de código abierto bajo los términos de la Licencia MIT.