Module: VerifactuRails::Huella

Defined in:
lib/verifactu_rails/huella.rb

Overview

Cálculo de la huella (hash) de los registros de facturación.

Referencia normativa: RD 1007/2023 art. 7, Orden HAC/1177/2024, y el documento "Detalle de las especificaciones técnicas para la generación de la huella o hash de los registros" publicado en la web de desarrolladores de la AEAT.

La cadena se construye como pares "Campo=valor" unidos por "&", SIN escapado de los valores, y se le aplica SHA-256 devuelto en hexadecimal MAYÚSCULAS.

Constant Summary collapse

PATRON_HUELLA =
/\A[0-9A-F]{64}\z/
CAMPOS_ALTA =
%w[
  IDEmisorFactura NumSerieFactura FechaExpedicionFactura
  TipoFactura CuotaTotal ImporteTotal Huella FechaHoraHusoGenRegistro
].freeze
CAMPOS_ANULACION =
%w[
  IDEmisorFacturaAnulada NumSerieFacturaAnulada FechaExpedicionFacturaAnulada
  Huella FechaHoraHusoGenRegistro
].freeze

Class Method Summary collapse

Class Method Details

.alta(**datos) ⇒ Object

Huella de un registro de facturación de ALTA.

Parameters:

  • huella_anterior (String, nil)

    huella del registro previo de la cadena. nil o "" únicamente para el PRIMER registro de ese SIF y NIF obligado. Ojo: la cadena es una sola por SIF+NIF, NO una por serie de facturación.



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

def alta(**datos)
  digerir(cadena_alta(**datos))
end

.anulacion(**datos) ⇒ Object

Huella de un registro de facturación de ANULACIÓN. Aquí huella_anterior es SIEMPRE obligatoria: una anulación nunca puede ser el primer registro de la cadena.



63
64
65
# File 'lib/verifactu_rails/huella.rb', line 63

def anulacion(**datos)
  digerir(cadena_anulacion(**datos))
end

.cadena_alta(id_emisor:, num_serie:, fecha_expedicion:, tipo_factura:, cuota_total:, importe_total:, fecha_hora_gen:, huella_anterior: nil) ⇒ Object

Cadena exacta sobre la que se calcula la huella de un ALTA.

Es pública a propósito y por dos motivos. Uno: cuando la AEAT rechaza por huella, lo primero que hay que comparar es ESTE string, no el digest. Dos: que los tests puedan afirmar sobre la cadena sin reconstruirla por su cuenta, que es como se quedan ciegos ante un cambio de normalizador.



45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/verifactu_rails/huella.rb', line 45

def cadena_alta(id_emisor:, num_serie:, fecha_expedicion:, tipo_factura:,
                cuota_total:, importe_total:, fecha_hora_gen:, huella_anterior: nil)
  pares = {
    'IDEmisorFactura'          => texto(id_emisor, 'IDEmisorFactura'),
    'NumSerieFactura'          => texto(num_serie, 'NumSerieFactura'),
    'FechaExpedicionFactura'   => fecha(fecha_expedicion),
    'TipoFactura'              => texto(tipo_factura, 'TipoFactura'),
    'CuotaTotal'               => Importe.formatear(cuota_total),
    'ImporteTotal'             => Importe.formatear(importe_total),
    'Huella'                   => encadenamiento(huella_anterior),
    'FechaHoraHusoGenRegistro' => marca_temporal(fecha_hora_gen)
  }
  serializar(pares, CAMPOS_ALTA)
end

.cadena_anulacion(id_emisor:, num_serie:, fecha_expedicion:, fecha_hora_gen:, huella_anterior:) ⇒ Object

Cadena exacta sobre la que se calcula la huella de una ANULACIÓN.



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/verifactu_rails/huella.rb', line 68

def cadena_anulacion(id_emisor:, num_serie:, fecha_expedicion:,
                     fecha_hora_gen:, huella_anterior:)
  if huella_anterior.nil? || huella_anterior.to_s.empty?
    raise ValidacionError,
          'Un registro de anulación exige huella_anterior: no puede iniciar cadena'
  end

  pares = {
    'IDEmisorFacturaAnulada'        => texto(id_emisor, 'IDEmisorFacturaAnulada'),
    'NumSerieFacturaAnulada'        => texto(num_serie, 'NumSerieFacturaAnulada'),
    'FechaExpedicionFacturaAnulada' => fecha(fecha_expedicion),
    'Huella'                        => encadenamiento(huella_anterior),
    'FechaHoraHusoGenRegistro'      => marca_temporal(fecha_hora_gen)
  }
  serializar(pares, CAMPOS_ANULACION)
end

.digerir(cadena) ⇒ Object



91
92
93
# File 'lib/verifactu_rails/huella.rb', line 91

def digerir(cadena)
  Digest::SHA256.hexdigest(cadena.encode(Encoding::UTF_8)).upcase
end

.serializar(pares, orden) ⇒ Object

Expone la cadena previa al hash. Imprescindible para depurar: cuando la AEAT rechaza por huella, lo primero es comparar ESTE string, no el digest.



87
88
89
# File 'lib/verifactu_rails/huella.rb', line 87

def serializar(pares, orden)
  orden.map { |campo| "#{campo}=#{pares.fetch(campo)}" }.join('&')
end