Class: VerifactuRails::Transporte

Inherits:
Object
  • Object
show all
Defined in:
lib/verifactu_rails/transporte.rb

Overview

Cliente HTTP con autenticación mutua TLS contra el servicio VERI*FACTU.

Sin Savon: el servicio es un único endpoint con un sobre SOAP fijo, así que Net::HTTP de la stdlib sobra. Nos ahorra httpi, wasabi, gyoku, akami y nori.

Constant Summary collapse

ENDPOINTS =

Preproducción y producción se corresponden uno a uno: prewww1 <-> www1, prewww2 <-> www2, prewww10 <-> www10 (este último, el de certificado de sello). Fuente: portal de pruebas externas de la AEAT.

Aviso operativo del propio portal: preproducción es para pruebas puntuales, NO para pruebas masivas ni para validaciones integradas en procesos de producción. Un uso que consideren abusivo puede acabar en bloqueo.

{
  [:pruebas, false]    => 'https://prewww1.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP',
  [:pruebas, true]     => 'https://prewww10.aeat.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP',
  [:produccion, false] => 'https://www1.agenciatributaria.gob.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP',
  [:produccion, true]  => 'https://www10.agenciatributaria.gob.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP'
}.freeze
SOAP_NS =
'http://schemas.xmlsoap.org/soap/envelope/'
DECLARACION_XML =

Una declaración solo puede ir al principio del documento, y Envio#to_xml emite la suya. Incrustarla tal cual dentro del Body producía un sobre MAL FORMADO con dos declaraciones, que la AEAT contestó con "Codigo.Error interno en el servidor": el fallo era de parseo, no de validación, así que el mensaje no orientaba en absoluto.

/\A\s*<\?xml[^>]*\?>\s*/

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(certificado:, entorno: :pruebas, sello: nil, url: nil, ca_file: nil, timeout: 30) ⇒ Transporte

Returns a new instance of Transporte.

Parameters:

  • sello (Boolean, nil) (defaults to: nil)

    fuerza el endpoint de sello de entidad. Si es nil se deduce del propio certificado.



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/verifactu_rails/transporte.rb', line 39

def initialize(certificado:, entorno: :pruebas, sello: nil, url: nil,
               ca_file: nil, timeout: 30)
  unless %i[pruebas produccion].include?(entorno)
    raise ValidacionError, "Entorno inválido: #{entorno.inspect} (usa :pruebas o :produccion)"
  end

  @certificado = Formato.objeto(certificado, 'certificado', Certificado)
  @entorno = entorno
  # Sin normalizar, un sello: 'S' llegaba a ENDPOINTS.fetch([entorno, 'S']) y
  # daba un KeyError, justo al lado de la comprobación de entorno que sí da
  # un error del dominio.
  @sello = sello.nil? ? @certificado.sello? : Formato.si_no(sello, 'sello') == 'S'
  @url = url || ENDPOINTS.fetch([entorno, @sello])
  @ca_file = ca_file
  @timeout = timeout
end

Instance Attribute Details

#certificadoObject (readonly)

Returns the value of attribute certificado.



35
36
37
# File 'lib/verifactu_rails/transporte.rb', line 35

def certificado
  @certificado
end

#entornoObject (readonly)

Returns the value of attribute entorno.



35
36
37
# File 'lib/verifactu_rails/transporte.rb', line 35

def entorno
  @entorno
end

#urlObject (readonly)

Returns the value of attribute url.



35
36
37
# File 'lib/verifactu_rails/transporte.rb', line 35

def url
  @url
end

Instance Method Details

#clienteObject



94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/verifactu_rails/transporte.rb', line 94

def cliente
  http = Net::HTTP.new(uri.host, uri.port)
  http.use_ssl = true
  http.cert = certificado.certificado
  http.key = certificado.clave
  http.extra_chain_cert = certificado.cadena if certificado.cadena.any?
  # NUNCA VERIFY_NONE. Si falla la verificación, el arreglo es aportar la
  # cadena de la CA correcta en ca_file, no desactivar la comprobación.
  http.verify_mode = OpenSSL::SSL::VERIFY_PEER
  http.ca_file = @ca_file if @ca_file
  http.open_timeout = @timeout
  http.read_timeout = @timeout
  http
end

#enviar(xml_registro) ⇒ Object

Envía el XML del registro ya construido. Devuelve la respuesta cruda: el parseo es responsabilidad de la capa superior.



60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/verifactu_rails/transporte.rb', line 60

def enviar(xml_registro)
  peticion = Net::HTTP::Post.new(uri)
  peticion['Content-Type'] = 'text/xml; charset=utf-8'
  peticion['SOAPAction'] = '""'
  peticion.body = envolver(xml_registro)

  respuesta = cliente.request(peticion)
  { codigo: respuesta.code.to_i, cuerpo: respuesta.body }
rescue OpenSSL::SSL::SSLError => e
  raise TransporteError, mensaje_ssl(e)
rescue Net::OpenTimeout, Net::ReadTimeout => e
  raise TransporteError, "Timeout contra #{uri.host}: #{e.class}"
end

#envolver(xml_registro) ⇒ Object



81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/verifactu_rails/transporte.rb', line 81

def envolver(xml_registro)
  cuerpo = xml_registro.to_s.sub(DECLARACION_XML, '')
  # document/literal según el WSDL: el Body lleva directamente el elemento,
  # sin envoltorio de operación.
  <<~XML
    <?xml version="1.0" encoding="UTF-8"?>
    <soapenv:Envelope xmlns:soapenv="#{SOAP_NS}">
      <soapenv:Header/>
      <soapenv:Body>#{cuerpo}</soapenv:Body>
    </soapenv:Envelope>
  XML
end

#sello?Boolean

Returns:

  • (Boolean)


56
# File 'lib/verifactu_rails/transporte.rb', line 56

def sello? = @sello