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.
- checksums.yaml +7 -0
- data/.devcontainer/Dockerfile +8 -0
- data/.devcontainer/devcontainer.json +12 -0
- data/.github/dependabot.yml +12 -0
- data/.github/workflows/ci.yml +16 -0
- data/.github/workflows/release.yml +37 -0
- data/.gitignore +8 -0
- data/.mise.toml +2 -0
- data/.rubocop.yml +8 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +19 -0
- data/CONTRIBUTING.md +48 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +209 -0
- data/Rakefile +10 -0
- data/SECURITY.md +14 -0
- data/arca.gemspec +40 -0
- data/bin/console +7 -0
- data/lib/arca/client.rb +29 -0
- data/lib/arca/core_ext/hash.rb +13 -0
- data/lib/arca/errors/error.rb +13 -0
- data/lib/arca/errors/network_error.rb +15 -0
- data/lib/arca/errors/response_error.rb +18 -0
- data/lib/arca/errors/server_error.rb +6 -0
- data/lib/arca/persona_service_a100.rb +51 -0
- data/lib/arca/persona_service_a4.rb +34 -0
- data/lib/arca/persona_service_a5.rb +46 -0
- data/lib/arca/type_conversions.rb +44 -0
- data/lib/arca/version.rb +5 -0
- data/lib/arca/w_cons_declaracion.rb +66 -0
- data/lib/arca/ws_constancia_inscripcion.rb +34 -0
- data/lib/arca/wsaa.rb +140 -0
- data/lib/arca/wscdc.rb +149 -0
- data/lib/arca/wsfe.rb +205 -0
- data/lib/arca/wsfecred.rb +438 -0
- data/lib/arca/wsrgiva.rb +72 -0
- data/lib/arca.rb +31 -0
- data/test/arca/client_test.rb +43 -0
- data/test/arca/core_ext/hash_test.rb +15 -0
- data/test/arca/persona_service_a100_test.rb +45 -0
- data/test/arca/persona_service_a4_test.rb +31 -0
- data/test/arca/persona_service_a5_test.rb +31 -0
- data/test/arca/test.crt +19 -0
- data/test/arca/test.key +28 -0
- data/test/arca/type_conversions_test.rb +43 -0
- data/test/arca/w_cons_declaracion_test.rb +86 -0
- data/test/arca/ws_constancia_inscripcion_test.rb +87 -0
- data/test/arca/wsaa_test.rb +80 -0
- data/test/arca/wscdc_test.rb +103 -0
- data/test/arca/wsfe_test.rb +319 -0
- data/test/arca/wsfecred_test.rb +162 -0
- data/test/arca/wsrgiva_test.rb +91 -0
- data/test/fixtures/wconsdeclaracion/detallada_estado/success.xml +24 -0
- data/test/fixtures/wconsdeclaracion/detallada_lista_declaraciones/por_fecha_success.xml +39 -0
- data/test/fixtures/wconsdeclaracion/detallada_lista_declaraciones/por_id_inexistente.xml +16 -0
- data/test/fixtures/wconsdeclaracion/detallada_lista_declaraciones/por_id_success.xml +29 -0
- data/test/fixtures/wconsdeclaracion/dummy/success.xml +12 -0
- data/test/fixtures/wconsdeclaracion/wconsdeclaracion.wsdl +2976 -0
- data/test/fixtures/ws_sr_constancia_inscripcion/dummy/success.xml +11 -0
- data/test/fixtures/ws_sr_constancia_inscripcion/get_persona/failure.xml +35 -0
- data/test/fixtures/ws_sr_constancia_inscripcion/get_persona/fault.xml +8 -0
- data/test/fixtures/ws_sr_constancia_inscripcion/get_persona/success.xml +53 -0
- data/test/fixtures/ws_sr_constancia_inscripcion/ws_constancia_inscripcion.wsdl +230 -0
- data/test/fixtures/ws_sr_padron_a100/company_types/success.xml +23 -0
- data/test/fixtures/ws_sr_padron_a100/dummy/success.xml +11 -0
- data/test/fixtures/ws_sr_padron_a100/jurisdictions/success.xml +31 -0
- data/test/fixtures/ws_sr_padron_a100/public_organisms/success.xml +23 -0
- data/test/fixtures/ws_sr_padron_a100.wsdl +125 -0
- data/test/fixtures/ws_sr_padron_a4/dummy/success.xml +11 -0
- data/test/fixtures/ws_sr_padron_a4/get_persona/success.xml +133 -0
- data/test/fixtures/ws_sr_padron_a4.wsdl +229 -0
- data/test/fixtures/ws_sr_padron_a5/dummy/success.xml +11 -0
- data/test/fixtures/ws_sr_padron_a5/get_persona/success.xml +62 -0
- data/test/fixtures/ws_sr_padron_a5.wsdl +283 -0
- data/test/fixtures/wsaa/login_cms/fault.xml +12 -0
- data/test/fixtures/wsaa/login_cms/success.xml +16 -0
- data/test/fixtures/wsaa/login_cms/token_expirado.xml +14 -0
- data/test/fixtures/wsaa/wsaa.wsdl +103 -0
- data/test/fixtures/wscdc/comprobante_constatar/success.xml +22 -0
- data/test/fixtures/wscdc/comprobante_constatar/with_errors.xml +28 -0
- data/test/fixtures/wscdc/comprobante_dummy/success.xml +11 -0
- data/test/fixtures/wscdc/comprobantes_modalidad_consultar/success.xml +22 -0
- data/test/fixtures/wscdc/comprobantes_tipo_consultar/success.xml +22 -0
- data/test/fixtures/wscdc/documentos_tipo_consultar/success.xml +16 -0
- data/test/fixtures/wscdc/opcionales_tipo_consultar/success.xml +14 -0
- data/test/fixtures/wscdc/wscdc.wsdl +305 -0
- data/test/fixtures/wsfe/fe_comp_consultar/success.xml +41 -0
- data/test/fixtures/wsfe/fe_comp_tot_x_request/success.xml +9 -0
- data/test/fixtures/wsfe/fe_comp_ultimo_autorizado/success.xml +11 -0
- data/test/fixtures/wsfe/fe_dummy/success.xml +11 -0
- data/test/fixtures/wsfe/fe_param_get_cotizacion/dolar.xml +13 -0
- data/test/fixtures/wsfe/fe_param_get_cotizacion/inexistente.xml +14 -0
- data/test/fixtures/wsfe/fe_param_get_ptos_venta/success.xml +22 -0
- data/test/fixtures/wsfe/fe_param_get_tipos_cbte/failure_1_error.xml +14 -0
- data/test/fixtures/wsfe/fe_param_get_tipos_cbte/failure_2_errors.xml +18 -0
- data/test/fixtures/wsfe/fe_param_get_tipos_cbte/success.xml +22 -0
- data/test/fixtures/wsfe/fe_param_get_tipos_concepto/success.xml +17 -0
- data/test/fixtures/wsfe/fe_param_get_tipos_doc/success.xml +16 -0
- data/test/fixtures/wsfe/fe_param_get_tipos_iva/success.xml +16 -0
- data/test/fixtures/wsfe/fe_param_get_tipos_monedas/success.xml +22 -0
- data/test/fixtures/wsfe/fe_param_get_tipos_opcional/success.xml +16 -0
- data/test/fixtures/wsfe/fe_param_get_tipos_tributos/success.xml +16 -0
- data/test/fixtures/wsfe/fecae_solicitar/autorizacion_1_cbte.xml +30 -0
- data/test/fixtures/wsfe/fecae_solicitar/autorizacion_2_cbtes.xml +41 -0
- data/test/fixtures/wsfe/fecae_solicitar/dos_observaciones.xml +40 -0
- data/test/fixtures/wsfe/fecae_solicitar/una_observacion.xml +36 -0
- data/test/fixtures/wsfe/fecaea_consultar/success.xml +17 -0
- data/test/fixtures/wsfe/fecaea_reg_informativo/informe_rtdo_parcial.xml +45 -0
- data/test/fixtures/wsfe/fecaea_sin_movimiento_informar/success.xml +12 -0
- data/test/fixtures/wsfe/fecaea_solicitar/caea_ya_otorgado.xml +18 -0
- data/test/fixtures/wsfe/fecaea_solicitar/error_distinto.xml +18 -0
- data/test/fixtures/wsfe/fecaea_solicitar/success.xml +17 -0
- data/test/fixtures/wsfe/wsfe.wsdl +1372 -0
- data/test/fixtures/wsfecred/aceptar_f_e_cred/success.xml +14 -0
- data/test/fixtures/wsfecred/consultar_comprobantes/success.xml +17 -0
- data/test/fixtures/wsfecred/consultar_cta_cte/success.xml +13 -0
- data/test/fixtures/wsfecred/consultar_ctas_ctes/success.xml +16 -0
- data/test/fixtures/wsfecred/consultar_tipos_formas_cancelacion/success.xml +14 -0
- data/test/fixtures/wsfecred/consultar_tipos_motivos_rechazo/success.xml +14 -0
- data/test/fixtures/wsfecred/consultar_tipos_retenciones/failure.xml +14 -0
- data/test/fixtures/wsfecred/consultar_tipos_retenciones/success.xml +14 -0
- data/test/fixtures/wsfecred/dummy/success.xml +11 -0
- data/test/fixtures/wsfecred/wsfecred.wsdl +147 -0
- data/test/fixtures/wsrgiva/consultar_constancia_por_lote/one_error.xml +14 -0
- data/test/fixtures/wsrgiva/consultar_constancia_por_lote/success.xml +26 -0
- data/test/fixtures/wsrgiva/dummy/success.xml +11 -0
- data/test/fixtures/wsrgiva/wsrgiva.wsdl +118 -0
- data/test/support/savon_extensions.rb +43 -0
- data/test/test_helper.rb +44 -0
- 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
|
data/lib/arca/version.rb
ADDED
|
@@ -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
|