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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c8d2177ff2a7410f72cf2b1cf7e730ed4b683a49980cf26d6dd861e6d08e1025
4
- data.tar.gz: 066b1a6a6ae7b089cc95a91d3ce034ddec3911d7cbc3c43fc16168a0e2bb85e5
3
+ metadata.gz: dd4d89db0b9d4d77abb2b5e6eb0e03d3169893d511a0e2b066f12e3c1d613e2c
4
+ data.tar.gz: 49b8a96793606351fa4a9ca0b137cab1f7efc2459ef5604d9efffe0a20c5b54f
5
5
  SHA512:
6
- metadata.gz: e2da9b334aa535351083be80a9734cd65777947990ccdbc48b369b71a97e9fc3bb5b6ea56e767493f9147c218159ae3bce47bcc713c469163b6d1033947fc1d1
7
- data.tar.gz: 8f8a371d97b9ee91b03d345b1f38973668e4e81b7294f7450d4a18e5f09306462ef9831b8a1d2dae7759edec5d0294f8c83b27512aba5077c827b4b9c61b021a
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.1)
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.0)
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.0)
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.0)
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
@@ -1,4 +1,4 @@
1
- Copyright (c) 2025 ARCA Kit
1
+ Copyright (c) 2025 driveton
2
2
 
3
3
  MIT License
4
4
 
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/AFIPen Argentina. Soporta facturación electrónica (WSFE), factura electrónica de crédito MiPyMEs (WSFeCred), autenticación WSAA, consulta de padrón de contribuyentes, constatación de comprobantes fiscales y todos los servicios tributarios de la API de AFIP. Ideal para aplicaciones Rails que necesitan generar facturas electrónicas y comprobantes en Argentina.'
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Arca
4
- VERSION = "1.0.1"
4
+ VERSION = "1.1.0"
5
5
  end
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 /(\d{2})(\d{2})$/, '\1:\2'
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)
@@ -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.1
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/AFIPen Argentina.
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: