arca.rb 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. checksums.yaml +7 -0
  2. data/.devcontainer/Dockerfile +8 -0
  3. data/.devcontainer/devcontainer.json +12 -0
  4. data/.github/dependabot.yml +12 -0
  5. data/.github/workflows/ci.yml +16 -0
  6. data/.github/workflows/release.yml +37 -0
  7. data/.gitignore +8 -0
  8. data/.mise.toml +2 -0
  9. data/.rubocop.yml +8 -0
  10. data/.ruby-version +1 -0
  11. data/CHANGELOG.md +19 -0
  12. data/CONTRIBUTING.md +48 -0
  13. data/Gemfile +4 -0
  14. data/LICENSE +22 -0
  15. data/README.md +209 -0
  16. data/Rakefile +10 -0
  17. data/SECURITY.md +14 -0
  18. data/arca.gemspec +40 -0
  19. data/bin/console +7 -0
  20. data/lib/arca/client.rb +29 -0
  21. data/lib/arca/core_ext/hash.rb +13 -0
  22. data/lib/arca/errors/error.rb +13 -0
  23. data/lib/arca/errors/network_error.rb +15 -0
  24. data/lib/arca/errors/response_error.rb +18 -0
  25. data/lib/arca/errors/server_error.rb +6 -0
  26. data/lib/arca/persona_service_a100.rb +51 -0
  27. data/lib/arca/persona_service_a4.rb +34 -0
  28. data/lib/arca/persona_service_a5.rb +46 -0
  29. data/lib/arca/type_conversions.rb +44 -0
  30. data/lib/arca/version.rb +5 -0
  31. data/lib/arca/w_cons_declaracion.rb +66 -0
  32. data/lib/arca/ws_constancia_inscripcion.rb +34 -0
  33. data/lib/arca/wsaa.rb +140 -0
  34. data/lib/arca/wscdc.rb +149 -0
  35. data/lib/arca/wsfe.rb +205 -0
  36. data/lib/arca/wsfecred.rb +438 -0
  37. data/lib/arca/wsrgiva.rb +72 -0
  38. data/lib/arca.rb +31 -0
  39. data/test/arca/client_test.rb +43 -0
  40. data/test/arca/core_ext/hash_test.rb +15 -0
  41. data/test/arca/persona_service_a100_test.rb +45 -0
  42. data/test/arca/persona_service_a4_test.rb +31 -0
  43. data/test/arca/persona_service_a5_test.rb +31 -0
  44. data/test/arca/test.crt +19 -0
  45. data/test/arca/test.key +28 -0
  46. data/test/arca/type_conversions_test.rb +43 -0
  47. data/test/arca/w_cons_declaracion_test.rb +86 -0
  48. data/test/arca/ws_constancia_inscripcion_test.rb +87 -0
  49. data/test/arca/wsaa_test.rb +80 -0
  50. data/test/arca/wscdc_test.rb +103 -0
  51. data/test/arca/wsfe_test.rb +319 -0
  52. data/test/arca/wsfecred_test.rb +162 -0
  53. data/test/arca/wsrgiva_test.rb +91 -0
  54. data/test/fixtures/wconsdeclaracion/detallada_estado/success.xml +24 -0
  55. data/test/fixtures/wconsdeclaracion/detallada_lista_declaraciones/por_fecha_success.xml +39 -0
  56. data/test/fixtures/wconsdeclaracion/detallada_lista_declaraciones/por_id_inexistente.xml +16 -0
  57. data/test/fixtures/wconsdeclaracion/detallada_lista_declaraciones/por_id_success.xml +29 -0
  58. data/test/fixtures/wconsdeclaracion/dummy/success.xml +12 -0
  59. data/test/fixtures/wconsdeclaracion/wconsdeclaracion.wsdl +2976 -0
  60. data/test/fixtures/ws_sr_constancia_inscripcion/dummy/success.xml +11 -0
  61. data/test/fixtures/ws_sr_constancia_inscripcion/get_persona/failure.xml +35 -0
  62. data/test/fixtures/ws_sr_constancia_inscripcion/get_persona/fault.xml +8 -0
  63. data/test/fixtures/ws_sr_constancia_inscripcion/get_persona/success.xml +53 -0
  64. data/test/fixtures/ws_sr_constancia_inscripcion/ws_constancia_inscripcion.wsdl +230 -0
  65. data/test/fixtures/ws_sr_padron_a100/company_types/success.xml +23 -0
  66. data/test/fixtures/ws_sr_padron_a100/dummy/success.xml +11 -0
  67. data/test/fixtures/ws_sr_padron_a100/jurisdictions/success.xml +31 -0
  68. data/test/fixtures/ws_sr_padron_a100/public_organisms/success.xml +23 -0
  69. data/test/fixtures/ws_sr_padron_a100.wsdl +125 -0
  70. data/test/fixtures/ws_sr_padron_a4/dummy/success.xml +11 -0
  71. data/test/fixtures/ws_sr_padron_a4/get_persona/success.xml +133 -0
  72. data/test/fixtures/ws_sr_padron_a4.wsdl +229 -0
  73. data/test/fixtures/ws_sr_padron_a5/dummy/success.xml +11 -0
  74. data/test/fixtures/ws_sr_padron_a5/get_persona/success.xml +62 -0
  75. data/test/fixtures/ws_sr_padron_a5.wsdl +283 -0
  76. data/test/fixtures/wsaa/login_cms/fault.xml +12 -0
  77. data/test/fixtures/wsaa/login_cms/success.xml +16 -0
  78. data/test/fixtures/wsaa/login_cms/token_expirado.xml +14 -0
  79. data/test/fixtures/wsaa/wsaa.wsdl +103 -0
  80. data/test/fixtures/wscdc/comprobante_constatar/success.xml +22 -0
  81. data/test/fixtures/wscdc/comprobante_constatar/with_errors.xml +28 -0
  82. data/test/fixtures/wscdc/comprobante_dummy/success.xml +11 -0
  83. data/test/fixtures/wscdc/comprobantes_modalidad_consultar/success.xml +22 -0
  84. data/test/fixtures/wscdc/comprobantes_tipo_consultar/success.xml +22 -0
  85. data/test/fixtures/wscdc/documentos_tipo_consultar/success.xml +16 -0
  86. data/test/fixtures/wscdc/opcionales_tipo_consultar/success.xml +14 -0
  87. data/test/fixtures/wscdc/wscdc.wsdl +305 -0
  88. data/test/fixtures/wsfe/fe_comp_consultar/success.xml +41 -0
  89. data/test/fixtures/wsfe/fe_comp_tot_x_request/success.xml +9 -0
  90. data/test/fixtures/wsfe/fe_comp_ultimo_autorizado/success.xml +11 -0
  91. data/test/fixtures/wsfe/fe_dummy/success.xml +11 -0
  92. data/test/fixtures/wsfe/fe_param_get_cotizacion/dolar.xml +13 -0
  93. data/test/fixtures/wsfe/fe_param_get_cotizacion/inexistente.xml +14 -0
  94. data/test/fixtures/wsfe/fe_param_get_ptos_venta/success.xml +22 -0
  95. data/test/fixtures/wsfe/fe_param_get_tipos_cbte/failure_1_error.xml +14 -0
  96. data/test/fixtures/wsfe/fe_param_get_tipos_cbte/failure_2_errors.xml +18 -0
  97. data/test/fixtures/wsfe/fe_param_get_tipos_cbte/success.xml +22 -0
  98. data/test/fixtures/wsfe/fe_param_get_tipos_concepto/success.xml +17 -0
  99. data/test/fixtures/wsfe/fe_param_get_tipos_doc/success.xml +16 -0
  100. data/test/fixtures/wsfe/fe_param_get_tipos_iva/success.xml +16 -0
  101. data/test/fixtures/wsfe/fe_param_get_tipos_monedas/success.xml +22 -0
  102. data/test/fixtures/wsfe/fe_param_get_tipos_opcional/success.xml +16 -0
  103. data/test/fixtures/wsfe/fe_param_get_tipos_tributos/success.xml +16 -0
  104. data/test/fixtures/wsfe/fecae_solicitar/autorizacion_1_cbte.xml +30 -0
  105. data/test/fixtures/wsfe/fecae_solicitar/autorizacion_2_cbtes.xml +41 -0
  106. data/test/fixtures/wsfe/fecae_solicitar/dos_observaciones.xml +40 -0
  107. data/test/fixtures/wsfe/fecae_solicitar/una_observacion.xml +36 -0
  108. data/test/fixtures/wsfe/fecaea_consultar/success.xml +17 -0
  109. data/test/fixtures/wsfe/fecaea_reg_informativo/informe_rtdo_parcial.xml +45 -0
  110. data/test/fixtures/wsfe/fecaea_sin_movimiento_informar/success.xml +12 -0
  111. data/test/fixtures/wsfe/fecaea_solicitar/caea_ya_otorgado.xml +18 -0
  112. data/test/fixtures/wsfe/fecaea_solicitar/error_distinto.xml +18 -0
  113. data/test/fixtures/wsfe/fecaea_solicitar/success.xml +17 -0
  114. data/test/fixtures/wsfe/wsfe.wsdl +1372 -0
  115. data/test/fixtures/wsfecred/aceptar_f_e_cred/success.xml +14 -0
  116. data/test/fixtures/wsfecred/consultar_comprobantes/success.xml +17 -0
  117. data/test/fixtures/wsfecred/consultar_cta_cte/success.xml +13 -0
  118. data/test/fixtures/wsfecred/consultar_ctas_ctes/success.xml +16 -0
  119. data/test/fixtures/wsfecred/consultar_tipos_formas_cancelacion/success.xml +14 -0
  120. data/test/fixtures/wsfecred/consultar_tipos_motivos_rechazo/success.xml +14 -0
  121. data/test/fixtures/wsfecred/consultar_tipos_retenciones/failure.xml +14 -0
  122. data/test/fixtures/wsfecred/consultar_tipos_retenciones/success.xml +14 -0
  123. data/test/fixtures/wsfecred/dummy/success.xml +11 -0
  124. data/test/fixtures/wsfecred/wsfecred.wsdl +147 -0
  125. data/test/fixtures/wsrgiva/consultar_constancia_por_lote/one_error.xml +14 -0
  126. data/test/fixtures/wsrgiva/consultar_constancia_por_lote/success.xml +26 -0
  127. data/test/fixtures/wsrgiva/dummy/success.xml +11 -0
  128. data/test/fixtures/wsrgiva/wsrgiva.wsdl +118 -0
  129. data/test/support/savon_extensions.rb +43 -0
  130. data/test/test_helper.rb +44 -0
  131. metadata +319 -0
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arca
4
+ class PersonaServiceA4
5
+ WSDL = {
6
+ development: "https://awshomo.afip.gov.ar/sr-padron/webservices/personaServiceA4?WSDL",
7
+ production: "https://aws.afip.gov.ar/sr-padron/webservices/personaServiceA4?WSDL",
8
+ test: "#{Root}/test/fixtures/ws_sr_padron_a4.wsdl"
9
+ }.freeze
10
+
11
+ attr_reader :wsaa
12
+
13
+ def initialize(options = {})
14
+ @cuit = options[:cuit]
15
+ @wsaa = WSAA.new options.merge(service: "ws_sr_padron_a4")
16
+ @client = Client.new Hash(options[:savon]).reverse_merge(wsdl: WSDL[@wsaa.env], soap_version: 1)
17
+ end
18
+
19
+ def dummy
20
+ request(:dummy)[:return]
21
+ end
22
+
23
+ def get_persona(id)
24
+ message = @wsaa.auth.merge(cuitRepresentada: @cuit, idPersona: id)
25
+ request(:get_persona, message)[:persona_return][:persona]
26
+ end
27
+
28
+ private
29
+
30
+ def request(action, body = nil)
31
+ @client.request(action, body).to_hash[:"#{action}_response"]
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arca
4
+ class PersonaServiceA5
5
+ WSDL = {
6
+ development: "https://awshomo.afip.gov.ar/sr-padron/webservices/personaServiceA5?WSDL",
7
+ production: "https://aws.afip.gov.ar/sr-padron/webservices/personaServiceA5?WSDL",
8
+ test: "#{Root}/test/fixtures/ws_sr_padron_a5.wsdl"
9
+ }.freeze
10
+
11
+ attr_reader :wsaa
12
+
13
+ def initialize(options = {})
14
+ @cuit = options[:cuit]
15
+ @wsaa = WSAA.new options.merge(service: "ws_sr_padron_a5")
16
+ @client = Client.new Hash(options[:savon]).reverse_merge(wsdl: WSDL[@wsaa.env], soap_version: 1)
17
+ end
18
+
19
+ def dummy
20
+ request(:dummy)[:return]
21
+ end
22
+
23
+ def get_persona(id)
24
+ message = @wsaa.auth.merge(cuitRepresentada: @cuit, idPersona: id)
25
+ request(:get_persona, message)[:persona_return]
26
+ end
27
+
28
+ # Request many personas in one SOAP call. ids: array of CUIT/id_persona.
29
+ # Returns array of persona_return hashes (same shape as get_persona per element).
30
+ def get_persona_list(ids)
31
+ ids = Array(ids)
32
+ return [] if ids.empty?
33
+
34
+ message = @wsaa.auth.merge(cuitRepresentada: @cuit, idPersona: ids.map(&:to_s))
35
+ raw = request(:get_persona_list, message)[:persona_list_return]
36
+ personas = raw[:persona]
37
+ Array.wrap(personas).compact
38
+ end
39
+
40
+ private
41
+
42
+ def request(action, body = nil)
43
+ @client.request(action, body).to_hash[:"#{action}_response"]
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arca
4
+ module TypeConversions
5
+ def r2x(x, types)
6
+ convert x, types, marshall_fn
7
+ end
8
+
9
+ def x2r(x, types)
10
+ convert x, types, parsing_fn
11
+ end
12
+
13
+ private
14
+
15
+ # Hace una conversión recursiva de tipo de todos los values según los tipos de las keys indicados en types
16
+ def convert(object, types, convert_fn)
17
+ case object
18
+ when Array
19
+ object.map { |e| convert e, types, convert_fn }
20
+ when Hash
21
+ object.to_h do |k, v|
22
+ [ k, v.is_a?(Hash) || v.is_a?(Array) ? convert(v, types, convert_fn) : convert_fn[types[k]].call(v) ]
23
+ end
24
+ else
25
+ object
26
+ end
27
+ end
28
+
29
+ def parsing_fn
30
+ @parsing_fn ||= Hash.new(proc { |v| v }).tap do |p|
31
+ p[:date] = proc { |v| ::Date.parse(v) rescue nil }
32
+ p[:integer] = proc(&:to_i)
33
+ p[:float] = proc(&:to_f)
34
+ p[:boolean] = proc { |v| v == "S" }
35
+ end
36
+ end
37
+
38
+ def marshall_fn
39
+ @marshall_fn ||= Hash.new(proc { |other| other }).tap do |p|
40
+ p[:date] = proc { |date| date.strftime("%Y%m%d") }
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arca
4
+ VERSION = "1.0.0"
5
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arca
4
+ class WConsDeclaracion
5
+ WSDL = {
6
+ development: "https://wsaduhomoext.afip.gob.ar/diav2/wconsdeclaracion/wconsdeclaracion.asmx?WSDL",
7
+ production: "https://webservicesadu.afip.gov.ar/DIAV2/wconsdeclaracion/wconsdeclaracion.asmx?WSDL",
8
+ test: "#{Root}/test/fixtures/wconsdeclaracion/wconsdeclaracion.wsdl"
9
+ }.freeze
10
+
11
+ attr_reader :wsaa
12
+
13
+ def initialize tipo_agente: "IMEX", rol: "IMEX", **options
14
+ @cuit, @tipo_agente, @rol = options[:cuit], tipo_agente, rol
15
+ @wsaa = WSAA.new(options.merge(service: "wconsdeclaracion"))
16
+ @client = Client.new(Hash(options[:savon]).reverse_merge(wsdl: WSDL[@wsaa.env]))
17
+ end
18
+
19
+ def dummy
20
+ request :dummy
21
+ end
22
+
23
+ def detallada_lista_declaraciones(identificador_declaracion: nil, fecha_oficializacion_desde: nil,
24
+ fecha_oficializacion_hasta: nil)
25
+ message = {
26
+ "argDetalladasListaParams" => {
27
+ "CuitImportadorExportador" => @cuit,
28
+ "IdentificadorDeclaracion" => identificador_declaracion,
29
+ "FechaOficializacionDesde" => fecha_oficializacion_desde&.iso8601,
30
+ "FechaOficializacionHasta" => fecha_oficializacion_hasta&.iso8601
31
+ }.compact
32
+ }
33
+ request(:detallada_lista_declaraciones, auth.merge(message))[:declaraciones][:declaracion]
34
+ end
35
+
36
+ def detallada_estado(identificador_declaracion)
37
+ message = { "argIdentificadorDestinacion" => identificador_declaracion }
38
+ request(:detallada_estado, auth.merge(message))[:estado]
39
+ end
40
+
41
+ private
42
+
43
+ def request(action, body = nil)
44
+ response = @client.request(action, body).to_hash[:"#{action}_response"][:"#{action}_result"]
45
+ if response[:lista_errores] && response[:lista_errores][:detalle_error][:codigo] != "0"
46
+ raise ResponseError, Array.wrap(response[:lista_errores][:detalle_error]).map { |e|
47
+ { code: e[:codigo], msg: e[:descripcion] }
48
+ }
49
+ else
50
+ response
51
+ end
52
+ end
53
+
54
+ def auth
55
+ {
56
+ "argWSAutenticacionEmpresa" => {
57
+ "Token" => @wsaa.auth[:token],
58
+ "Sign" => @wsaa.auth[:sign],
59
+ "CuitEmpresaConectada" => @cuit,
60
+ "TipoAgente" => @tipo_agente,
61
+ "Rol" => @rol
62
+ }
63
+ }
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Arca
4
+ class WSConstanciaInscripcion
5
+ WSDL = {
6
+ development: "https://awshomo.afip.gov.ar/sr-padron/webservices/personaServiceA5?WSDL",
7
+ production: "https://aws.afip.gov.ar/sr-padron/webservices/personaServiceA5?WSDL",
8
+ test: "#{Root}/test/fixtures/ws_sr_constancia_inscripcion/ws_constancia_inscripcion.wsdl"
9
+ }.freeze
10
+
11
+ attr_reader :wsaa
12
+
13
+ def initialize(options = {})
14
+ @cuit = options[:cuit]
15
+ @wsaa = WSAA.new options.merge(service: "ws_sr_constancia_inscripcion")
16
+ @client = Client.new Hash(options[:savon]).reverse_merge(wsdl: WSDL[@wsaa.env], soap_version: 1)
17
+ end
18
+
19
+ def dummy
20
+ request(:dummy)[:return]
21
+ end
22
+
23
+ def get_persona(id)
24
+ message = @wsaa.auth.merge(cuit_representada: @cuit, id_persona: id)
25
+ request(:get_persona, message)[:persona_return]
26
+ end
27
+
28
+ private
29
+
30
+ def request(action, body = nil)
31
+ @client.request(action, body).to_hash[:"#{action}_response"]
32
+ end
33
+ end
34
+ end
data/lib/arca/wsaa.rb ADDED
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Arca
6
+ class WSAA
7
+ attr_reader :key, :cert, :service, :ta, :cuit, :client, :env
8
+
9
+ WSDL = {
10
+ development: "https://wsaahomo.afip.gov.ar/ws/services/LoginCms?wsdl",
11
+ production: "https://wsaa.afip.gov.ar/ws/services/LoginCms?wsdl",
12
+ test: "#{Root}/test/fixtures/wsaa/wsaa.wsdl"
13
+ }
14
+
15
+ def initialize(options = {})
16
+ @env = (options[:env] || :test).to_sym
17
+ @key = options[:key]
18
+ @cert = options[:cert]
19
+ @service = options[:service] || "wsfe"
20
+ @ttl = options[:ttl] || 2400
21
+ @cuit = options[:cuit]
22
+ @client = Client.new Hash(options[:savon]).reverse_merge(wsdl: WSDL[@env])
23
+ @ta_path = options[:ta_path] || default_ta_path
24
+ end
25
+
26
+ def generar_tra(service, ttl)
27
+ xml = Builder::XmlMarkup.new indent: 2
28
+ xml.instruct!
29
+ xml.loginTicketRequest version: 1 do
30
+ xml.header do
31
+ xml.uniqueId Time.now.to_i
32
+ xml.generationTime xsd_datetime Time.now - ttl
33
+ xml.expirationTime xsd_datetime Time.now + ttl
34
+ end
35
+ xml.service service
36
+ end
37
+ end
38
+
39
+ def firmar_tra(tra, key, crt)
40
+ key = OpenSSL::PKey::RSA.new key
41
+ crt = OpenSSL::X509::Certificate.new crt
42
+ OpenSSL::PKCS7.sign crt, key, tra
43
+ end
44
+
45
+ def codificar_tra(pkcs7)
46
+ pkcs7.to_pem.lines.to_a[1..-2].join
47
+ end
48
+
49
+ def tra(key, cert, service, ttl)
50
+ codificar_tra firmar_tra(generar_tra(service, ttl), key, cert)
51
+ end
52
+
53
+ def login
54
+ response = @client.request :login_cms, in0: tra(@key, @cert, @service, @ttl)
55
+ ta = Nokogiri::XML(Nokogiri::XML(response.to_xml).text)
56
+ {
57
+ token: ta.css("token").text,
58
+ sign: ta.css("sign").text,
59
+ generation_time: from_xsd_datetime(ta.css("generationTime").text),
60
+ expiration_time: from_xsd_datetime(ta.css("expirationTime").text)
61
+ }
62
+ end
63
+
64
+ def auth
65
+ ta = obtener_y_cachear_ta
66
+ { token: ta[:token], sign: ta[:sign] }
67
+ end
68
+
69
+ private
70
+
71
+ # Previene el error 'El CEE ya posee un TA valido para el acceso al WSN solicitado' que se genera cuando se pide el token varias veces en poco tiempo
72
+ def obtener_y_cachear_ta
73
+ @ta ||= restore_ta
74
+ if ta_expirado? @ta
75
+ @ta = login
76
+ persist_ta @ta
77
+ end
78
+ @ta
79
+ end
80
+
81
+ def restore_ta
82
+ return nil unless File.exist?(@ta_path) && !File.empty?(@ta_path)
83
+
84
+ data = JSON.parse(File.read(@ta_path))
85
+ {
86
+ token: data["token"],
87
+ sign: data["sign"],
88
+ generation_time: data["generation_time"] && Time.parse(data["generation_time"]),
89
+ expiration_time: data["expiration_time"] && Time.parse(data["expiration_time"])
90
+ }
91
+ rescue JSON::ParserError, ArgumentError
92
+ nil
93
+ end
94
+
95
+ def ta_expirado?(ta)
96
+ if ta.nil?
97
+ true
98
+ elsif ta[:expiration_time].nil?
99
+ true
100
+ else
101
+ ta[:expiration_time] <= Time.now
102
+ end
103
+ end
104
+
105
+ def persist_ta(ta)
106
+ dir = File.dirname(@ta_path)
107
+ FileUtils.mkdir_p(dir)
108
+
109
+ payload = {
110
+ "token" => ta[:token],
111
+ "sign" => ta[:sign],
112
+ "generation_time" => ta[:generation_time]&.iso8601,
113
+ "expiration_time" => ta[:expiration_time]&.iso8601
114
+ }
115
+
116
+ temp_path = "#{@ta_path}.#{Process.pid}.tmp"
117
+ File.write(temp_path, JSON.generate(payload))
118
+ File.rename(temp_path, @ta_path)
119
+ rescue StandardError
120
+ File.delete(temp_path) if defined?(temp_path) && File.exist?(temp_path)
121
+ raise
122
+ end
123
+
124
+ def xsd_datetime(time)
125
+ time.strftime("%Y-%m-%dT%H:%M:%S%z").sub /(\d{2})(\d{2})$/, '\1:\2'
126
+ end
127
+
128
+ def from_xsd_datetime(str)
129
+ Time.parse(str) rescue nil
130
+ end
131
+
132
+ def default_ta_path
133
+ # Sanitize to prevent path traversal if cuit/service come from input
134
+ cuit_safe = @cuit.to_s.gsub(/\D/, "")
135
+ service_safe = @service.to_s.gsub(/[^a-z0-9_]/, "")
136
+ basename = "#{cuit_safe}-#{@env}-#{service_safe}-ta.json"
137
+ File.join(Dir.pwd, "tmp", basename)
138
+ end
139
+ end
140
+ end
data/lib/arca/wscdc.rb ADDED
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ # WSCDC: Web Service de Constatación de Comprobantes (ARCA).
4
+ # Verificación de comprobantes electrónicos (CAE, CAEA, CAI).
5
+ # Documentación: Servicio de Constatación de Comprobantes.
6
+ module Arca
7
+ class WSCDC
8
+ WSDL = {
9
+ development: "https://wswhomo.afip.gov.ar/WSCDC/service.asmx?WSDL",
10
+ production: "https://servicios1.afip.gov.ar/WSCDC/service.asmx?WSDL",
11
+ test: "#{Root}/test/fixtures/wscdc/wscdc.wsdl"
12
+ }.freeze
13
+
14
+ include TypeConversions
15
+
16
+ attr_reader :wsaa, :cuit
17
+
18
+ def initialize(options = {})
19
+ @cuit = normalize_cuit(options[:cuit])
20
+ @wsaa = WSAA.new options.merge(service: "wscdc")
21
+ @client = Client.new Hash(options[:savon]).reverse_merge(
22
+ wsdl: WSDL[@wsaa.env],
23
+ soap_version: 1,
24
+ convert_request_keys_to: :camelcase
25
+ )
26
+ end
27
+
28
+ def dummy
29
+ r = raw_request(:comprobante_dummy, nil)
30
+ result = r[:comprobante_dummy_result]
31
+ {
32
+ app_server: result[:app_server],
33
+ db_server: result[:db_server],
34
+ auth_server: result[:auth_server]
35
+ }
36
+ end
37
+
38
+ # Constata un comprobante. cmp_req: hash con cbte_modo, cuit_emisor, pto_vta,
39
+ # cbte_tipo, cbte_nro, cbte_fch, imp_total, cod_autorizacion y opcionalmente
40
+ # doc_tipo_receptor, doc_nro_receptor, opcionales.
41
+ # Returns hash con :cmp_resp, :resultado ('A'|'R'), :observaciones, :fch_proceso, :errors.
42
+ def comprobante_constatar(cmp_req)
43
+ message = { auth: auth, cmp_req: cmp_req }
44
+ r = raw_request(:comprobante_constatar, message)
45
+ result = r[:comprobante_constatar_result]
46
+ if result[:errors] && (errs = Array.wrap(result[:errors][:err])).any?
47
+ raise ResponseError, errs.map { |e| { code: e[:code], msg: e[:msg] } }
48
+ end
49
+
50
+ {
51
+ cmp_resp: result[:cmp_resp],
52
+ resultado: result[:resultado],
53
+ observaciones: parse_observaciones(result[:observaciones]),
54
+ fch_proceso: result[:fch_proceso],
55
+ errors: parse_errors(result[:errors]),
56
+ events: parse_events(result[:events])
57
+ }
58
+ end
59
+
60
+ # Modalidades de autorización (CAE, CAEA, CAI).
61
+ def comprobantes_modalidad_consultar
62
+ r = request(:comprobantes_modalidad_consultar)
63
+ get_array(r, :fac_mod_tipo)
64
+ end
65
+
66
+ # Tipos de comprobante.
67
+ def comprobantes_tipo_consultar
68
+ r = request(:comprobantes_tipo_consultar)
69
+ x2r get_array(r, :cbte_tipo), id: :integer, fch_desde: :date, fch_hasta: :date
70
+ end
71
+
72
+ # Tipos de documento.
73
+ def documentos_tipo_consultar
74
+ r = request(:documentos_tipo_consultar)
75
+ x2r get_array(r, :doc_tipo), id: :integer, fch_desde: :date, fch_hasta: :date
76
+ end
77
+
78
+ # Tipos de datos opcionales.
79
+ def opcionales_tipo_consultar
80
+ r = request(:opcionales_tipo_consultar)
81
+ get_array(r, :opcional_tipo)
82
+ end
83
+
84
+ private
85
+
86
+ def request(action, body = nil)
87
+ message = body || { auth: auth }
88
+ r = raw_request(action, message)
89
+ result_key = :"#{action}_result"
90
+ result = r[result_key]
91
+ if result && result[:errors] && (errs = Array.wrap(result[:errors][:err])).any?
92
+ raise ResponseError, errs.map { |e| { code: e[:code], msg: e[:msg] } }
93
+ end
94
+
95
+ result
96
+ end
97
+
98
+ def auth
99
+ @wsaa.auth.merge(cuit: cuit)
100
+ end
101
+
102
+ def raw_request(action, body = nil)
103
+ resp = @client.request(action, body).to_hash[:"#{action}_response"]
104
+ raise ServerError, "Unexpected response structure" unless resp
105
+
106
+ resp
107
+ end
108
+
109
+ def get_array(response, array_element)
110
+ if response && response[:result_get]
111
+ Array.wrap(response[:result_get][array_element])
112
+ else
113
+ []
114
+ end
115
+ end
116
+
117
+ def parse_observaciones(observaciones)
118
+ if observaciones && observaciones[:obs]
119
+ Array.wrap(observaciones[:obs])
120
+ else
121
+ []
122
+ end
123
+ end
124
+
125
+ def parse_errors(errors)
126
+ if errors && errors[:err]
127
+ Array.wrap(errors[:err])
128
+ else
129
+ []
130
+ end
131
+ end
132
+
133
+ def parse_events(events)
134
+ if events && events[:evt]
135
+ Array.wrap(events[:evt])
136
+ else
137
+ []
138
+ end
139
+ end
140
+
141
+ def normalize_cuit(value)
142
+ if value.nil? || value == ""
143
+ 0
144
+ else
145
+ value.to_s.gsub(/\D/, "").to_i
146
+ end
147
+ end
148
+ end
149
+ end