arca.rb 1.0.1 → 1.1.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 +4 -4
- data/CHANGELOG.md +10 -0
- data/Gemfile.lock +4 -4
- data/LICENSE +1 -1
- data/README.md +6 -1
- data/arca.gemspec +1 -1
- data/lib/arca/version.rb +1 -1
- data/lib/arca/wsaa.rb +49 -4
- data/test/arca/wsaa_test.rb +67 -0
- metadata +2 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dd4d89db0b9d4d77abb2b5e6eb0e03d3169893d511a0e2b066f12e3c1d613e2c
|
|
4
|
+
data.tar.gz: 49b8a96793606351fa4a9ca0b137cab1f7efc2459ef5604d9efffe0a20c5b54f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4b6060fe8e15d82e5ca2cf8cfa5f7651899ae10661df16a389b6cb43ddcfbed79f6303b17a126b654d40e1b5830ba78263f6a20f7af6d11a1fe36f89eee8c459
|
|
7
|
+
data.tar.gz: 02242344d53082a0b5b7f3b6aba1f004184b8aa8ac64e91ac9ed07b7f623a95191fe82582f0acd14256d616d5b2928f59b07471c73351887dcf7fb4b8af7342f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Arca Changelog
|
|
2
2
|
|
|
3
|
+
## [1.1.0]
|
|
4
|
+
|
|
5
|
+
- **WSAA:** Store opcional para TA. Podés pasar `store:` con un objeto que implemente `read(cuit:, env:, service:)` y `write(cuit:, env:, service:, ta:, expires_at:)` para guardar el token en tu backend (p. ej. DB por tenant con cifrado). Sin `store`, se sigue usando el archivo JSON en `tmp/`.
|
|
6
|
+
|
|
7
|
+
## [1.0.2] - 2025-02-09
|
|
8
|
+
|
|
9
|
+
- **Errores:** En WSAA, `persist_ta` ahora encapsula fallos en `Arca::ServerError` en lugar de re-levantar excepciones crudas (mejor encapsulación y menos riesgo de filtrar datos sensibles en logs).
|
|
10
|
+
- **Test:** Añadido test que verifica que `persist_ta` levanta `ServerError` con la excepción original en `cause`.
|
|
11
|
+
- Actualización de gems/dependencias.
|
|
12
|
+
|
|
3
13
|
## [1.0.1] - 2025-02-05
|
|
4
14
|
- Fix ruta hardcodeada de certificados SSL CA. Usar OpenSSL::X509::DEFAULT_CERT_FILE en lugar de la ruta hardcodeada
|
|
5
15
|
/etc/ssl/certs/ca-certificates.crt para soportar múltiples plataformas.
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
arca.rb (1.0
|
|
4
|
+
arca.rb (1.1.0)
|
|
5
5
|
activesupport
|
|
6
6
|
builder
|
|
7
7
|
httpclient
|
|
@@ -39,7 +39,7 @@ GEM
|
|
|
39
39
|
connection_pool (3.0.2)
|
|
40
40
|
date (3.5.1)
|
|
41
41
|
drb (2.2.3)
|
|
42
|
-
faraday (2.14.
|
|
42
|
+
faraday (2.14.1)
|
|
43
43
|
faraday-net_http (>= 2.0, < 3.5)
|
|
44
44
|
json
|
|
45
45
|
logger
|
|
@@ -58,7 +58,7 @@ GEM
|
|
|
58
58
|
i18n (1.14.8)
|
|
59
59
|
concurrent-ruby (~> 1.0)
|
|
60
60
|
io-console (0.8.2)
|
|
61
|
-
json (2.18.
|
|
61
|
+
json (2.18.1)
|
|
62
62
|
language_server-protocol (3.17.0.5)
|
|
63
63
|
lint_roller (1.1.0)
|
|
64
64
|
logger (1.7.0)
|
|
@@ -111,7 +111,7 @@ GEM
|
|
|
111
111
|
reline (0.6.3)
|
|
112
112
|
io-console (~> 0.5)
|
|
113
113
|
rexml (3.4.4)
|
|
114
|
-
rubocop (1.84.
|
|
114
|
+
rubocop (1.84.1)
|
|
115
115
|
json (~> 2.3)
|
|
116
116
|
language_server-protocol (~> 3.17.0.2)
|
|
117
117
|
lint_roller (~> 1.1.0)
|
data/LICENSE
CHANGED
data/README.md
CHANGED
|
@@ -75,7 +75,7 @@ Opción del constructor: `env: :development` o `env: :production` (symbol o stri
|
|
|
75
75
|
|
|
76
76
|
## Uso
|
|
77
77
|
|
|
78
|
-
Opciones comunes del constructor: `env`, `cuit`, `key`, `cert`. Opcionales: `savon` (hash pasado a Savon), `ta_path` (WSAA), etc.
|
|
78
|
+
Opciones comunes del constructor: `env`, `cuit`, `key`, `cert`. Opcionales: `savon` (hash pasado a Savon), `ta_path` (WSAA, path del archivo TA), `store` (WSAA, objeto con `read`/`write` para guardar TA en backend propio), etc.
|
|
79
79
|
|
|
80
80
|
### Cliente de bajo nivel (`Arca::Client`)
|
|
81
81
|
|
|
@@ -112,6 +112,11 @@ wsaa = Arca::WSAA.new(env: :development, cuit: '20123456789', key: key, cert: ce
|
|
|
112
112
|
auth = wsaa.auth # => { token: '...', sign: '...' }
|
|
113
113
|
```
|
|
114
114
|
|
|
115
|
+
Opcionalmente podés pasar un **store** para guardar el TA en tu propio backend (por ejemplo DB por tenant con cifrado). Sin `store`, se usa un archivo JSON en `tmp/`. El store debe implementar:
|
|
116
|
+
|
|
117
|
+
- **`read(cuit:, env:, service:)`** — devuelve `nil` o un Hash con claves simbólicas: `:token`, `:sign`, `:generation_time`, `:expiration_time` (Time o string ISO8601).
|
|
118
|
+
- **`write(cuit:, env:, service:, ta:, expires_at:)`** — se invoca después de un `#login` exitoso; `ta` es el hash del TA y `expires_at` el Time de vencimiento.
|
|
119
|
+
|
|
115
120
|
### WSFE (Factura Electrónica)
|
|
116
121
|
```ruby
|
|
117
122
|
ws = Arca::WSFE.new(env: :development, cuit: '20123456789', key: key, cert: cert)
|
data/arca.gemspec
CHANGED
|
@@ -12,7 +12,7 @@ Gem::Specification.new do |s|
|
|
|
12
12
|
s.email = [ 'hola@arcakit.dev' ]
|
|
13
13
|
s.homepage = 'https://arcakit.dev'
|
|
14
14
|
s.summary = 'Cliente Ruby para webservices de AFIP/ARCA: facturación electrónica, comprobantes y servicios tributarios de Argentina'
|
|
15
|
-
s.description = 'Cliente Ruby para integrar webservices SOAP de ARCA/
|
|
15
|
+
s.description = 'Cliente Ruby para integrar webservices SOAP de ARCA/AFIP en Argentina.'
|
|
16
16
|
s.license = 'MIT'
|
|
17
17
|
|
|
18
18
|
s.files = `git ls-files`.split("\n")
|
data/lib/arca/version.rb
CHANGED
data/lib/arca/wsaa.rb
CHANGED
|
@@ -4,7 +4,7 @@ require "json"
|
|
|
4
4
|
|
|
5
5
|
module Arca
|
|
6
6
|
class WSAA
|
|
7
|
-
attr_reader :key, :cert, :service, :ta, :cuit, :client, :env
|
|
7
|
+
attr_reader :key, :cert, :service, :ta, :cuit, :client, :env, :store
|
|
8
8
|
|
|
9
9
|
WSDL = {
|
|
10
10
|
development: "https://wsaahomo.afip.gov.ar/ws/services/LoginCms?wsdl",
|
|
@@ -20,6 +20,7 @@ module Arca
|
|
|
20
20
|
@ttl = options[:ttl] || 2400
|
|
21
21
|
@cuit = options[:cuit]
|
|
22
22
|
@client = Client.new Hash(options[:savon]).reverse_merge(wsdl: WSDL[@env])
|
|
23
|
+
@store = options[:store]
|
|
23
24
|
@ta_path = options[:ta_path] || default_ta_path
|
|
24
25
|
end
|
|
25
26
|
|
|
@@ -79,6 +80,10 @@ module Arca
|
|
|
79
80
|
end
|
|
80
81
|
|
|
81
82
|
def restore_ta
|
|
83
|
+
if @store
|
|
84
|
+
return restore_ta_from_store
|
|
85
|
+
end
|
|
86
|
+
|
|
82
87
|
return nil unless File.exist?(@ta_path) && !File.empty?(@ta_path)
|
|
83
88
|
|
|
84
89
|
data = JSON.parse(File.read(@ta_path))
|
|
@@ -92,6 +97,27 @@ module Arca
|
|
|
92
97
|
nil
|
|
93
98
|
end
|
|
94
99
|
|
|
100
|
+
def restore_ta_from_store
|
|
101
|
+
h = @store.read(cuit: @cuit, env: @env, service: @service)
|
|
102
|
+
return nil if h.nil? || !h.is_a?(Hash)
|
|
103
|
+
|
|
104
|
+
{
|
|
105
|
+
token: h[:token],
|
|
106
|
+
sign: h[:sign],
|
|
107
|
+
generation_time: normalize_ta_time(h[:generation_time]),
|
|
108
|
+
expiration_time: normalize_ta_time(h[:expiration_time])
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def normalize_ta_time(value)
|
|
113
|
+
return nil if value.nil?
|
|
114
|
+
return value if value.is_a?(Time)
|
|
115
|
+
|
|
116
|
+
Time.parse(value.to_s)
|
|
117
|
+
rescue ArgumentError
|
|
118
|
+
nil
|
|
119
|
+
end
|
|
120
|
+
|
|
95
121
|
def ta_expirado?(ta)
|
|
96
122
|
if ta.nil?
|
|
97
123
|
true
|
|
@@ -103,6 +129,25 @@ module Arca
|
|
|
103
129
|
end
|
|
104
130
|
|
|
105
131
|
def persist_ta(ta)
|
|
132
|
+
if @store
|
|
133
|
+
begin
|
|
134
|
+
@store.write(
|
|
135
|
+
cuit: @cuit,
|
|
136
|
+
env: @env,
|
|
137
|
+
service: @service,
|
|
138
|
+
ta: ta,
|
|
139
|
+
expires_at: ta[:expiration_time]
|
|
140
|
+
)
|
|
141
|
+
rescue StandardError => e
|
|
142
|
+
raise ServerError, e
|
|
143
|
+
end
|
|
144
|
+
return
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
persist_ta_to_file(ta)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def persist_ta_to_file(ta)
|
|
106
151
|
dir = File.dirname(@ta_path)
|
|
107
152
|
FileUtils.mkdir_p(dir)
|
|
108
153
|
|
|
@@ -116,13 +161,13 @@ module Arca
|
|
|
116
161
|
temp_path = "#{@ta_path}.#{Process.pid}.tmp"
|
|
117
162
|
File.write(temp_path, JSON.generate(payload))
|
|
118
163
|
File.rename(temp_path, @ta_path)
|
|
119
|
-
rescue StandardError
|
|
164
|
+
rescue StandardError => e
|
|
120
165
|
File.delete(temp_path) if defined?(temp_path) && File.exist?(temp_path)
|
|
121
|
-
raise
|
|
166
|
+
raise ServerError, e
|
|
122
167
|
end
|
|
123
168
|
|
|
124
169
|
def xsd_datetime(time)
|
|
125
|
-
time.strftime("%Y-%m-%dT%H:%M:%S%z").sub
|
|
170
|
+
time.strftime("%Y-%m-%dT%H:%M:%S%z").sub(/(\d{2})(\d{2})$/, '\1:\2')
|
|
126
171
|
end
|
|
127
172
|
|
|
128
173
|
def from_xsd_datetime(str)
|
data/test/arca/wsaa_test.rb
CHANGED
|
@@ -76,5 +76,72 @@ module Arca
|
|
|
76
76
|
subject.auth
|
|
77
77
|
assert_equal "t2", subject.ta[:token]
|
|
78
78
|
end
|
|
79
|
+
|
|
80
|
+
def test_persist_ta_encapsula_errores_en_server_error
|
|
81
|
+
File.stubs(:write).raises(Errno::EACCES.new("Permission denied"))
|
|
82
|
+
ta = { token: "t", sign: "s", generation_time: Time.now, expiration_time: Time.now + 60 }
|
|
83
|
+
ws = WSAA.new(wsdl: Arca::WSFE::WSDL[:test])
|
|
84
|
+
|
|
85
|
+
error = assert_raises(ServerError) { ws.send(:persist_ta, ta) }
|
|
86
|
+
assert_equal Errno::EACCES, error.cause.class
|
|
87
|
+
assert_match(/Permission denied/, error.cause.message)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def test_auth_con_store_usa_read_y_no_llama_login_si_ta_valido
|
|
91
|
+
Time.stubs(:now).returns(Time.local(2010, 1, 1))
|
|
92
|
+
ta = { token: "stored-token", sign: "stored-sign", generation_time: Time.now, expiration_time: Time.now + 3600 }
|
|
93
|
+
store = Object.new
|
|
94
|
+
store.expects(:read).with(cuit: "20123456789", env: :test, service: "wsfe").returns(ta)
|
|
95
|
+
|
|
96
|
+
ws = WSAA.new(cuit: "20123456789", env: :test, service: "wsfe", wsdl: Arca::WSFE::WSDL[:test], store: store)
|
|
97
|
+
ws.expects(:login).never
|
|
98
|
+
|
|
99
|
+
assert_equal({ token: "stored-token", sign: "stored-sign" }, ws.auth)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def test_auth_con_store_llama_login_y_write_cuando_read_devuelve_nil
|
|
103
|
+
Time.stubs(:now).returns(Time.local(2010, 1, 1))
|
|
104
|
+
ta = { token: "new-token", sign: "new-sign", generation_time: Time.now, expiration_time: Time.now + 60 }
|
|
105
|
+
store = Object.new
|
|
106
|
+
store.expects(:read).with(cuit: "20123456789", env: :test, service: "wsfe").returns(nil)
|
|
107
|
+
store.expects(:write).with(cuit: "20123456789", env: :test, service: "wsfe", ta: ta, expires_at: ta[:expiration_time]).returns(nil)
|
|
108
|
+
|
|
109
|
+
ws = WSAA.new(cuit: "20123456789", env: :test, service: "wsfe", wsdl: Arca::WSFE::WSDL[:test], store: store)
|
|
110
|
+
ws.expects(:login).returns(ta)
|
|
111
|
+
|
|
112
|
+
ws.auth
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def test_auth_con_store_normaliza_tiempos_iso8601_de_read
|
|
116
|
+
Time.stubs(:now).returns(Time.local(2010, 1, 1))
|
|
117
|
+
gen_str = "2010-01-01T00:00:00-03:00"
|
|
118
|
+
exp_str = "2010-01-01T02:00:00-03:00"
|
|
119
|
+
store = Object.new
|
|
120
|
+
store.expects(:read).with(cuit: "20123456789", env: :test, service: "wsfe").returns({ token: "t", sign: "s", generation_time: gen_str, expiration_time: exp_str })
|
|
121
|
+
|
|
122
|
+
ws = WSAA.new(cuit: "20123456789", env: :test, service: "wsfe", wsdl: Arca::WSFE::WSDL[:test], store: store)
|
|
123
|
+
ws.expects(:login).never
|
|
124
|
+
|
|
125
|
+
ws.auth
|
|
126
|
+
assert_instance_of(Time, ws.ta[:generation_time])
|
|
127
|
+
assert_instance_of(Time, ws.ta[:expiration_time])
|
|
128
|
+
assert_equal(Time.parse(gen_str), ws.ta[:generation_time])
|
|
129
|
+
assert_equal(Time.parse(exp_str), ws.ta[:expiration_time])
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def test_persist_ta_con_store_encapsula_errores_en_server_error
|
|
133
|
+
ta = { token: "t", sign: "s", generation_time: Time.now, expiration_time: Time.now + 60 }
|
|
134
|
+
store = Class.new do
|
|
135
|
+
def read(*) nil end
|
|
136
|
+
def write(*) raise Errno::EACCES, "Permission denied" end
|
|
137
|
+
end.new
|
|
138
|
+
|
|
139
|
+
ws = WSAA.new(cuit: "1", env: :test, service: "wsfe", wsdl: Arca::WSFE::WSDL[:test], store: store)
|
|
140
|
+
assert_same store, ws.store
|
|
141
|
+
|
|
142
|
+
error = assert_raises(ServerError) { ws.send(:persist_ta, ta) }
|
|
143
|
+
assert_equal Errno::EACCES, error.cause.class
|
|
144
|
+
assert_match(/Permission denied/, error.cause.message)
|
|
145
|
+
end
|
|
79
146
|
end
|
|
80
147
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: arca.rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Arca Kit
|
|
@@ -149,11 +149,7 @@ dependencies:
|
|
|
149
149
|
- - "~>"
|
|
150
150
|
- !ruby/object:Gem::Version
|
|
151
151
|
version: 2.15.0
|
|
152
|
-
description: Cliente Ruby para integrar webservices SOAP de ARCA/
|
|
153
|
-
Soporta facturación electrónica (WSFE), factura electrónica de crédito MiPyMEs (WSFeCred),
|
|
154
|
-
autenticación WSAA, consulta de padrón de contribuyentes, constatación de comprobantes
|
|
155
|
-
fiscales y todos los servicios tributarios de la API de AFIP. Ideal para aplicaciones
|
|
156
|
-
Rails que necesitan generar facturas electrónicas y comprobantes en Argentina.
|
|
152
|
+
description: Cliente Ruby para integrar webservices SOAP de ARCA/AFIP en Argentina.
|
|
157
153
|
email:
|
|
158
154
|
- hola@arcakit.dev
|
|
159
155
|
executables:
|