Module: VerifactuRails::Importe

Defined in:
lib/verifactu_rails/importe.rb

Overview

Formateo canónico de importes monetarios.

REGLA DE ORO: el string que produce este módulo es el que debe ir TANTO en la cadena de la huella COMO en el XML. Si divergen aunque sea en un decimal, la AEAT recalcula la huella sobre lo que recibe en el XML y rechaza el registro. Por eso el formateo vive aquí y en un solo sitio.

Constant Summary collapse

PATRON =
/\A-?\d{1,12}\.\d{2}\z/

Class Method Summary collapse

Class Method Details

.formatear(valor) ⇒ Object

Normaliza a string con exactamente 2 decimales y punto como separador. Acepta BigDecimal, Integer, Rational o String; rechaza Float por imprecisión binaria (0.1 + 0.2 no es 0.3 y aquí eso es un rechazo AEAT).



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/verifactu_rails/importe.rb', line 22

def formatear(valor)
  case valor
  when Float
    raise ValidacionError,
          'No se admiten Float en importes: usa BigDecimal, Integer o String ' \
          "(recibido: #{valor.inspect})"
  when BigDecimal then decimal = valor
  when Integer    then decimal = valor.to_d
  when Rational   then decimal = valor.to_d(20)
  when String     then decimal = parsear_string(valor)
  when nil        then raise ValidacionError, 'Importe requerido'
  else
    raise ValidacionError, "Tipo de importe no admitido: #{valor.class}"
  end

  # ROUND_HALF_UP explícito, y NO porque Ruby traiga otro por defecto: el
  # suyo ya es HALF_UP, tanto en BigDecimal.mode como en Float#round. El
  # motivo es que BigDecimal.mode es estado GLOBAL del proceso: cualquier
  # gema de la aplicación puede cambiarlo, y entonces todos los importes
  # redondearían distinto y con ellos las huellas, sin que nadie relacionara
  # una cosa con la otra. Pasándolo en la llamada, da igual lo que haga el
  # resto. HALF_UP es además el que aplica la normativa fiscal española.
  resultado = decimal.round(2, BigDecimal::ROUND_HALF_UP).to_s('F')
  resultado = format('%.2f', resultado.to_d) # asegura los 2 decimales

  unless resultado.match?(PATRON)
    raise ValidacionError, "Importe fuera de rango o mal formado: #{resultado}"
  end

  # -0.00 no existe fiscalmente y rompería la comparación de huellas
  resultado == '-0.00' ? '0.00' : resultado
end

.porcentaje(valor, campo) ⇒ Object

sf:Tipo2.2Type, para los porcentajes: patrón \d1,3(.\d0,2)? — SIN signo y con tres dígitos enteros como mucho. Es un tipo distinto de sf:ImporteSgn12.2Type, así que formatear con formatear a secas dejaba pasar un -21.00 o un 1000.00 que luego rechaza el esquema.



59
60
61
62
63
64
65
66
67
68
# File 'lib/verifactu_rails/importe.rb', line 59

def porcentaje(valor, campo)
  cadena = formatear(valor)
  unless cadena.match?(/\A\d{1,3}\.\d{2}\z/)
    raise ValidacionError,
          "#{campo} debe ser un porcentaje sin signo y de hasta 3 dígitos " \
          "enteros (recibido: #{cadena})"
  end

  cadena
end