afipws 2.2.0 → 2.3.1

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: 35d82abb92bacac0765fa6ff1165144285d88b6de45ffb84ae70fa99a8c5bd34
4
- data.tar.gz: 5dfb329baf6446010559789d0afbcf24b1de9dbe8a5c7121f5b3dec932ef1d4e
3
+ metadata.gz: ff3d04f1454611ad13f2c7eb6fd2e8c80f0d190bc2c6ae850bf0ec6ce16625cf
4
+ data.tar.gz: 17a1b14cb8f01aabe85a5e4b497082401592cbda4d7e4a03eae4eff1a40c1296
5
5
  SHA512:
6
- metadata.gz: 9c3a00c50955d67e1a4c5bf53965a5b5d2b5ec305c0d4fff28beb8f54339d7d0bd2e9d17145dccdf101f1608f4a0c0420da2e75e2ab8ef8fdfaae7163d142547
7
- data.tar.gz: e3cfc5f3648b0ab30686f6a7848557b42f332eb12e38216a7b2ee9da69cc3a2d0dfc437477b80d35e2f9b003f699ea3fdd988cbdf9dfd23f85f7c980df3fca17
6
+ metadata.gz: 2bdb1d9a44bd828b27bc4bb69078143fe7256f8dcd93dd3af8613a736f60d9bfc795e6677f88ba4e94577303bc61805988c2bd0d11ab57647dcf1cc77356fc88
7
+ data.tar.gz: ec6eb8132f4848a6413be77f6b2b854966a6858fdc4443a24dc164bd637532988f3267a132f7b84b30258bd56aeee6d191bcf6ddeef71ea5aa78810b4d5bb9ca
@@ -4,7 +4,7 @@ jobs:
4
4
  test:
5
5
  runs-on: ubuntu-latest
6
6
  steps:
7
- - uses: actions/checkout@v3
7
+ - uses: actions/checkout@v6
8
8
  - name: Set up Ruby
9
9
  uses: ruby/setup-ruby@v1
10
10
  - name: Install dependencies
data/.gitignore CHANGED
@@ -1,8 +1,7 @@
1
1
  pkg/*
2
2
  *.gem
3
3
  .bundle
4
- spec/manual/*.key
5
- spec/manual/*.crt
6
4
  .DS_Store
7
5
  tmp
8
- /.direnv
6
+ /.direnv
7
+ /keys
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.7.5
1
+ 3.3.4
data/AGENTS.md ADDED
@@ -0,0 +1,35 @@
1
+ # Repository Guidelines
2
+
3
+ ## Project Structure & Module Organization
4
+
5
+ This repository is the `afipws` Ruby gem, a client for AFIP web services. Public entry points and service implementations live in `lib/`: `lib/afipws.rb` loads the gem, while files such as `wsfe.rb`, `wsaa.rb`, and `persona_service_a100.rb` implement individual services. Shared errors and extensions belong in `lib/afipws/errors/` and `lib/afipws/core_ext/`.
6
+
7
+ RSpec tests mirror production paths under `spec/afipws/` (for example, `lib/afipws/client.rb` maps to `spec/afipws/client_spec.rb`). Keep recorded SOAP XML and WSDL inputs in the corresponding `spec/fixtures/` service directory. Use the README and manual smoke test for runnable usage examples.
8
+
9
+ ## Build, Test, and Development Commands
10
+
11
+ Use mise to select Ruby 3.3.4 and run the supported development tasks:
12
+
13
+ ```sh
14
+ mise install # install the configured Ruby runtime
15
+ mise run setup # bundle install
16
+ mise run test # run the complete RSpec suite through Rake
17
+ mise run console # start an interactive console with lib/ loaded
18
+ bundle exec guard # rerun affected specs while editing
19
+ ```
20
+
21
+ CI performs `bundle install` followed by `bundle exec rake`; run the same test command before submitting changes.
22
+
23
+ ## Coding Style & Naming Conventions
24
+
25
+ Follow the existing Ruby style: two-space indentation, `snake_case` methods/files, `CamelCase` classes/modules, and namespaced implementation under `Afipws`. Keep service classes and fixture paths descriptive and aligned (for example, `WSFE` and `spec/fixtures/wsfe/`). Prefer the project’s established RSpec should-style assertions when editing nearby specs.
26
+
27
+ Rubocop configuration is in `.rubocop.yml`: line length is limited to 120 characters, and several modern-style cops are intentionally disabled to preserve the existing codebase style. Do not reformat unrelated files.
28
+
29
+ ## Testing Guidelines
30
+
31
+ Add or update a `*_spec.rb` for every behavior change. Tests must not call live AFIP endpoints: use Savon mocks and XML fixtures via `fixture('service/action/result')`. Cover successful responses, SOAP/HTTP failures, and retry behavior when relevant. Run `mise run test` after fixture or service changes.
32
+
33
+ ## Commit & Pull Request Guidelines
34
+
35
+ Use short, imperative commit subjects, as in `Add examples`, `Update ruby`, or `Handle Savon::HTTPError`; dependency updates use `Bump <gem> from <old> to <new>`. Keep commits focused. Pull requests should explain the service/API impact, list tests run, link the relevant issue when available, and include sanitized request/response examples when changing SOAP behavior. Never commit production certificates, private keys, or tokens.
data/CHANGELOG.md CHANGED
@@ -4,13 +4,23 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/).
6
6
 
7
- ## [2.0.0] - 2020-10-28
7
+ ## [2.3.1] - 2026-07-26
8
8
 
9
- ### Added
9
+ - Fix WSFE production TLS compatibility with ARCA's current endpoint.
10
+ - Preserve custom SSL cipher overrides and limit the compatibility setting to production WSFE.
11
+ - Improve documentation and add a manual production smoke test.
10
12
 
11
- - Long overdue CHANGELOG file.
12
- - Include `httpclient` dependency so we can wrap network errors.
13
+ ## [2.3.0] - 2024-07-13
14
+
15
+ - Upgrade Ruby to 3.3.4
16
+ - Upgrade Savon to 2.15.1
17
+
18
+ ## [2.2.0] - 2023-03-19
13
19
 
14
- ### Changed
20
+ - Handle Savon::HTTPError
15
21
 
22
+ ## [2.0.0] - 2020-10-28
23
+
24
+ - Long overdue CHANGELOG file.
25
+ - Include `httpclient` dependency so we can wrap network errors.
16
26
  - Rename `Afipws::WSError` to `Afipws::Error`, and create subclasses to distinguish between different types of errors.
data/Gemfile.lock CHANGED
@@ -1,34 +1,50 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- afipws (2.2.0)
4
+ afipws (2.3.1)
5
5
  activesupport
6
6
  builder
7
7
  httpclient
8
8
  nokogiri
9
- savon (~> 2.12.0)
9
+ savon (~> 2.15.0)
10
10
 
11
11
  GEM
12
12
  remote: http://rubygems.org/
13
13
  specs:
14
- activesupport (7.0.4.3)
14
+ activesupport (7.1.3.4)
15
+ base64
16
+ bigdecimal
15
17
  concurrent-ruby (~> 1.0, >= 1.0.2)
18
+ connection_pool (>= 2.2.5)
19
+ drb
16
20
  i18n (>= 1.6, < 2)
17
21
  minitest (>= 5.1)
22
+ mutex_m
18
23
  tzinfo (~> 2.0)
19
- addressable (2.8.0)
20
- public_suffix (>= 2.0.2, < 5.0)
21
- akami (1.3.1)
24
+ addressable (2.8.7)
25
+ public_suffix (>= 2.0.2, < 7.0)
26
+ akami (1.3.3)
27
+ base64
22
28
  gyoku (>= 0.4.0)
23
29
  nokogiri
24
- ast (2.4.2)
25
- builder (3.2.4)
30
+ base64 (0.2.0)
31
+ bigdecimal (3.1.8)
32
+ builder (3.3.0)
26
33
  coderay (1.1.3)
27
- concurrent-ruby (1.2.2)
28
- diff-lcs (1.3)
29
- ffi (1.15.5)
34
+ concurrent-ruby (1.3.3)
35
+ connection_pool (2.4.1)
36
+ date (3.3.4)
37
+ diff-lcs (1.5.1)
38
+ drb (2.2.1)
39
+ faraday (2.10.0)
40
+ faraday-net_http (>= 2.0, < 3.2)
41
+ logger
42
+ faraday-net_http (3.1.0)
43
+ net-http
44
+ ffi (1.17.0)
45
+ ffi (1.17.0-x86_64-linux-gnu)
30
46
  formatador (1.1.0)
31
- guard (2.18.0)
47
+ guard (2.18.1)
32
48
  formatador (>= 0.2.4)
33
49
  listen (>= 2.7, < 4.0)
34
50
  lumberjack (>= 1.0.12, < 2.0)
@@ -42,88 +58,106 @@ GEM
42
58
  guard (~> 2.1)
43
59
  guard-compat (~> 1.1)
44
60
  rspec (>= 2.99.0, < 4.0)
45
- gyoku (1.3.1)
61
+ gyoku (1.4.0)
46
62
  builder (>= 2.1.2)
63
+ rexml (~> 3.0)
47
64
  httpclient (2.8.3)
48
- httpi (2.5.0)
49
- rack
50
- socksify
51
- i18n (1.12.0)
65
+ httpi (4.0.3)
66
+ base64
67
+ mutex_m
68
+ nkf
69
+ rack (>= 2.0, < 4)
70
+ i18n (1.14.5)
52
71
  concurrent-ruby (~> 1.0)
53
- jaro_winkler (1.5.4)
54
- listen (3.7.1)
72
+ listen (3.9.0)
55
73
  rb-fsevent (~> 0.10, >= 0.10.3)
56
74
  rb-inotify (~> 0.9, >= 0.9.10)
57
- lumberjack (1.2.8)
58
- method_source (1.0.0)
59
- minitest (5.18.0)
75
+ logger (1.6.0)
76
+ lumberjack (1.2.10)
77
+ mail (2.8.1)
78
+ mini_mime (>= 0.1.1)
79
+ net-imap
80
+ net-pop
81
+ net-smtp
82
+ method_source (1.1.0)
83
+ mini_mime (1.1.5)
84
+ mini_portile2 (2.8.7)
85
+ minitest (5.24.1)
60
86
  mocha (0.9.10)
61
87
  rake
88
+ mutex_m (0.2.0)
62
89
  nenv (0.3.0)
63
- nokogiri (1.13.10-arm64-darwin)
90
+ net-http (0.4.1)
91
+ uri
92
+ net-imap (0.4.14)
93
+ date
94
+ net-protocol
95
+ net-pop (0.1.2)
96
+ net-protocol
97
+ net-protocol (0.2.2)
98
+ timeout
99
+ net-smtp (0.5.0)
100
+ net-protocol
101
+ nkf (0.2.0)
102
+ nokogiri (1.16.6)
103
+ mini_portile2 (~> 2.8.2)
64
104
  racc (~> 1.4)
65
- nori (2.6.0)
105
+ nokogiri (1.16.6-x86_64-linux)
106
+ racc (~> 1.4)
107
+ nori (2.7.0)
108
+ bigdecimal
66
109
  notiffany (0.1.3)
67
110
  nenv (~> 0.1)
68
111
  shellany (~> 0.0)
69
- parallel (1.21.0)
70
- parser (3.1.0.0)
71
- ast (~> 2.4.1)
72
- pry (0.14.1)
112
+ pry (0.14.2)
73
113
  coderay (~> 1.1)
74
114
  method_source (~> 1.0)
75
- public_suffix (4.0.6)
76
- racc (1.6.1)
77
- rack (3.0.7)
78
- rainbow (3.1.1)
115
+ public_suffix (6.0.0)
116
+ racc (1.8.0)
117
+ rack (3.1.7)
79
118
  rake (13.0.1)
80
- rb-fsevent (0.11.1)
81
- rb-inotify (0.10.1)
119
+ rb-fsevent (0.11.2)
120
+ rb-inotify (0.11.1)
82
121
  ffi (~> 1.0)
83
- rexml (3.2.5)
84
- rspec (3.7.0)
85
- rspec-core (~> 3.7.0)
86
- rspec-expectations (~> 3.7.0)
87
- rspec-mocks (~> 3.7.0)
88
- rspec-core (3.7.1)
89
- rspec-support (~> 3.7.0)
90
- rspec-expectations (3.7.0)
122
+ rexml (3.3.1)
123
+ strscan
124
+ rspec (3.13.0)
125
+ rspec-core (~> 3.13.0)
126
+ rspec-expectations (~> 3.13.0)
127
+ rspec-mocks (~> 3.13.0)
128
+ rspec-core (3.13.0)
129
+ rspec-support (~> 3.13.0)
130
+ rspec-expectations (3.13.1)
91
131
  diff-lcs (>= 1.2.0, < 2.0)
92
- rspec-support (~> 3.7.0)
93
- rspec-mocks (3.7.0)
132
+ rspec-support (~> 3.13.0)
133
+ rspec-mocks (3.13.1)
94
134
  diff-lcs (>= 1.2.0, < 2.0)
95
- rspec-support (~> 3.7.0)
96
- rspec-support (3.7.1)
97
- rubocop (0.80.1)
98
- jaro_winkler (~> 1.5.1)
99
- parallel (~> 1.10)
100
- parser (>= 2.7.0.1)
101
- rainbow (>= 2.2.2, < 4.0)
102
- rexml
103
- ruby-progressbar (~> 1.7)
104
- unicode-display_width (>= 1.4.0, < 1.7)
105
- ruby-progressbar (1.11.0)
106
- savon (2.12.1)
135
+ rspec-support (~> 3.13.0)
136
+ rspec-support (3.13.1)
137
+ savon (2.15.1)
107
138
  akami (~> 1.2)
108
139
  builder (>= 2.1.2)
109
140
  gyoku (~> 1.2)
110
- httpi (~> 2.3)
141
+ httpi (>= 4, < 5)
142
+ mail (~> 2.5)
111
143
  nokogiri (>= 1.8.1)
112
144
  nori (~> 2.4)
113
- wasabi (~> 3.4)
145
+ wasabi (>= 3.7, < 6)
114
146
  shellany (0.0.1)
115
- socksify (1.7.1)
116
- thor (1.2.1)
147
+ strscan (3.1.0)
148
+ thor (1.3.1)
149
+ timeout (0.4.1)
117
150
  tzinfo (2.0.6)
118
151
  concurrent-ruby (~> 1.0)
119
- unicode-display_width (1.6.1)
120
- wasabi (3.7.0)
152
+ uri (0.13.0)
153
+ wasabi (5.0.2)
121
154
  addressable
122
- httpi (~> 2.0)
123
- nokogiri (>= 1.4.2)
155
+ faraday (~> 2.8)
156
+ nokogiri (>= 1.13.9)
124
157
 
125
158
  PLATFORMS
126
159
  ruby
160
+ x86_64-linux
127
161
 
128
162
  DEPENDENCIES
129
163
  afipws!
@@ -131,7 +165,6 @@ DEPENDENCIES
131
165
  mocha
132
166
  pry
133
167
  rspec
134
- rubocop (~> 0.80.0)
135
168
 
136
169
  BUNDLED WITH
137
- 2.1.4
170
+ 2.5.15
data/README.md CHANGED
@@ -1,44 +1,153 @@
1
1
  # Afipws
2
2
 
3
- Ruby client para los web services de la AFIP.
3
+ Cliente Ruby para los web services de ARCA (antes AFIP), con soporte para los
4
+ entornos de homologación y producción.
4
5
 
5
6
  [![Build Status](https://github.com/eeng/afipws/actions/workflows/ci.yml/badge.svg)](https://github.com/eeng/afipws/actions/workflows/ci.yml)
6
7
 
7
- ## Servicios Disponibles
8
+ ## Servicios disponibles
8
9
 
9
- - wsaa (WSAA)
10
- - wsfe (WSFE)
11
- - ws_sr_constancia_inscripcion (WSConstanciaInscripcion)
12
- - ws_sr_padron_a100 (PersonaServiceA100)
13
- - ws_sr_padron_a4 (PersonaServiceA4)
14
- - ws_sr_padron_a5 (PersonaServiceA5)
15
- - wconsdeclaracion (WConsDeclaracion)
10
+ | Clase | Servicio WSAA | Uso |
11
+ | --- | --- | --- |
12
+ | `WSAA` | — | Autenticación y tickets de acceso |
13
+ | `WSFE` | `wsfe` | Facturación electrónica WSFEv1 |
14
+ | `WSConstanciaInscripcion` | `ws_sr_constancia_inscripcion` | Constancia de inscripción |
15
+ | `PersonaServiceA4` | `ws_sr_padron_a4` | Consultas de padrón |
16
+ | `PersonaServiceA5` | `ws_sr_padron_a5` | Consultas de padrón |
17
+ | `PersonaServiceA100` | `ws_sr_padron_a100` | Parámetros del padrón |
18
+ | `WConsDeclaracion` | `wconsdeclaracion` | Consultas de declaraciones aduaneras |
16
19
 
17
- ## Uso
20
+ Cada servicio requiere que el certificado esté autorizado para ese servicio en
21
+ ARCA. Autorizar `wsfe` no autoriza automáticamente los servicios de padrón ni
22
+ los demás WSN.
18
23
 
19
- Primero hay que crear la clave privada y obtener el certificado correspondiente según los pasos indicados [aquí](http://www.afip.gov.ar/ws/WSAA/cert-req-howto.txt).
20
-
21
- Luego hay que instalar la librería:
24
+ ## Instalación
22
25
 
23
26
  ```
24
27
  gem install afipws
25
28
  ```
26
29
 
27
- Y por último usamos el web service de esta forma:
30
+ ## Certificados y autenticación
31
+
32
+ ARCA utiliza certificados X.509 para autenticar al cliente mediante WSAA.
33
+ Homologación y producción usan certificados y autorizaciones independientes:
34
+
35
+ 1. Generá una clave privada y un CSR. El `serialNumber` del CSR debe contener
36
+ la misma CUIT con la que se ingresa a ARCA.
37
+ 2. Para homologación, ingresá con clave fiscal a **WSASS – Autoservicio de
38
+ Acceso a APIs de Homologación**, elegí **Nuevo Certificado**, cargá el CSR y
39
+ guardá el certificado PEM devuelto.
40
+ 3. En WSASS, creá una autorización para cada servicio que vayas a consumir.
41
+ 4. Para producción, gestioná el certificado mediante el **Administrador de
42
+ Certificados Digitales** y asociá cada servicio desde el **Administrador de
43
+ Relaciones de Clave Fiscal**.
44
+
45
+ Documentación oficial: [WSAA](https://www.afip.gob.ar/ws/documentacion/wsaa.asp),
46
+ [certificados](https://www.arca.gob.ar/ws/documentacion/certificados.asp) y
47
+ [catálogo de web services](https://www.afip.gob.ar/ws/documentacion/catalogo.asp).
48
+
49
+ ### Generar una clave y un CSR
50
+
51
+ No compartas ni commitees la clave privada.
52
+
53
+ ```sh
54
+ mkdir -p keys
55
+ openssl genrsa -out keys/development.key 2048
56
+ openssl req -new \
57
+ -key keys/development.key \
58
+ -subj "/C=AR/O=[organizacion]/CN=[alias]/serialNumber=CUIT [cuit]" \
59
+ -out keys/development.csr
60
+ ```
61
+
62
+ El certificado y la clave deben corresponderse. Se puede verificar sin revelar
63
+ su contenido:
64
+
65
+ ```sh
66
+ openssl x509 -in keys/development.crt -noout -subject -dates
67
+ openssl x509 -in keys/development.crt -noout -modulus | openssl md5
68
+ openssl rsa -in keys/development.key -noout -modulus | openssl md5
69
+ ```
70
+
71
+ ### Ejemplo WSFE
72
+
73
+ El siguiente ejemplo consulta la cotización del dólar en homologación:
28
74
 
29
75
  ```ruby
30
76
  require 'afipws'
31
- ws = Afipws::WSFE.new env: :development, cuit: '...', key: File.read('test.key'), cert: File.read('test.crt')
77
+
78
+ ws = Afipws::WSFE.new(
79
+ env: :development,
80
+ cuit: 'YOUR_CUIT',
81
+ key: File.read('path/to/development.key'),
82
+ cert: File.read('path/to/development.crt')
83
+ )
84
+
32
85
  puts ws.cotizacion 'DOL'
33
86
  ```
34
87
 
35
- Ver specs para más ejemplos.
88
+ Para producción, cambiá `env: :development` por `env: :production` y usá el
89
+ certificado de producción correspondiente.
90
+
91
+ Los endpoints y WSDL se seleccionan automáticamente según `env`. La
92
+ documentación oficial de [facturación electrónica](https://www.afip.gob.ar/ws/documentacion/ws-factura-electronica.asp)
93
+ incluye el manual de WSFEv1 y sus operaciones.
94
+
95
+ ### WSFE en producción
96
+
97
+ El endpoint de producción de WSFE utiliza actualmente parámetros Diffie-Hellman
98
+ que OpenSSL rechaza con su nivel de seguridad predeterminado. `WSFE` aplica
99
+ automáticamente una compatibilidad limitada a ese endpoint (`DEFAULT@SECLEVEL=1`):
100
+ mantiene TLS 1.2, la verificación del certificado y no modifica otros servicios.
101
+ El ajuste se puede reemplazar pasando `savon: {ssl_ciphers: '...'}`; debe usarse
102
+ únicamente mientras ARCA mantenga este problema de infraestructura.
103
+
104
+ ## Desarrollo
105
+
106
+ Este proyecto usa [mise](https://mise.jdx.dev/) para seleccionar Ruby y ejecutar las tareas de desarrollo. El archivo `.ruby-version` fija la versión de Ruby requerida.
107
+
108
+ ```sh
109
+ mise install
110
+ mise run setup
111
+ mise run test
112
+ ```
113
+
114
+ También se puede abrir una consola interactiva con `mise run console`.
115
+
116
+ ### Smoke test manual
36
117
 
37
- ## Contributing
118
+ El script `script/smoke_test.rb` ejecuta consultas de solo lectura contra el
119
+ entorno de homologación. Requiere un certificado vigente y autorizado para los
120
+ servicios consultados:
121
+
122
+ ```sh
123
+ ARCA_CUIT='...' \
124
+ ARCA_KEY='path/to/development.key' \
125
+ ARCA_CERT='path/to/development.crt' \
126
+ bundle exec ruby -Ilib script/smoke_test.rb
127
+ ```
128
+
129
+ ```sh
130
+ ARCA_ENV=production \
131
+ ARCA_CUIT='...' ARCA_KEY='path/to/production.key' \
132
+ ARCA_CERT='path/to/production.crt' \
133
+ bundle exec ruby -Ilib script/smoke_test.rb
134
+ ```
135
+
136
+ Las consultas de padrón requieren además `ARCA_PERSONA_ID`. La consulta de
137
+ `wconsdeclaracion` es opcional y requiere `ARCA_RUN_WCONS=1`, además de los
138
+ valores correctos para `ARCA_TIPO_AGENTE` y `ARCA_ROL`.
139
+
140
+ ## Contribuir
141
+
142
+ Las contribuciones son bienvenidas. Consultá [AGENTS.md](AGENTS.md) para la
143
+ estructura del proyecto, estilo de código, pruebas y criterios para commits y
144
+ pull requests.
145
+
146
+ Antes de abrir un pull request, ejecutá la suite completa:
147
+
148
+ ```sh
149
+ mise run test
150
+ ```
38
151
 
39
- 1. Fork it
40
- 2. Create your feature branch (`git checkout -b my-new-feature`)
41
- 3. Test, test, test (`guard`)
42
- 4. Commit your changes (`git commit -am 'Add some feature'`)
43
- 5. Push to the branch (`git push origin my-new-feature`)
44
- 6. Create new Pull Request
152
+ Incluí una descripción breve del cambio y las pruebas realizadas. No incluyas
153
+ certificados, claves privadas ni credenciales en commits o ejemplos.
data/afipws.gemspec CHANGED
@@ -21,10 +21,9 @@ Gem::Specification.new do |s|
21
21
  s.add_development_dependency 'rspec'
22
22
  s.add_development_dependency 'mocha'
23
23
  s.add_development_dependency 'guard-rspec'
24
- s.add_development_dependency 'rubocop', '~> 0.80.0'
25
24
  s.add_development_dependency 'pry'
26
25
  s.add_dependency "builder"
27
- s.add_dependency "savon", '~> 2.12.0'
26
+ s.add_dependency "savon", '~> 2.15.0'
28
27
  s.add_dependency "httpclient"
29
28
  s.add_dependency "nokogiri"
30
29
  s.add_dependency "activesupport"
data/lib/afipws/client.rb CHANGED
@@ -1,4 +1,20 @@
1
1
  module Afipws
2
+ # HTTPI normally expands cipher strings into cipher names before handing
3
+ # them to HTTPClient, which drops OpenSSL security-level modifiers. This
4
+ # marker lets the affected client preserve an explicit cipher expression.
5
+ class RawSSLCipherString < String
6
+ end
7
+
8
+ module PreserveRawSSLCipherString
9
+ def ciphers=(ciphers)
10
+ return @ciphers = ciphers.to_s if ciphers.is_a?(RawSSLCipherString)
11
+
12
+ super
13
+ end
14
+ end
15
+
16
+ HTTPI::Auth::SSL.prepend PreserveRawSSLCipherString
17
+
2
18
  class Client
3
19
  def initialize savon_options
4
20
  @savon = Savon.client savon_options.reverse_merge(soap_version: 2, ssl_version: :TLSv1_2)
@@ -1,3 +1,3 @@
1
1
  module Afipws
2
- VERSION = '2.2.0'
2
+ VERSION = '2.3.1'
3
3
  end
data/lib/afipws/wsfe.rb CHANGED
@@ -1,5 +1,10 @@
1
1
  module Afipws
2
2
  class WSFE
3
+ # ARCA's production WSFE endpoint currently uses DH parameters rejected by
4
+ # OpenSSL's default security level. Keep this exception local to WSFE's
5
+ # production client; do not weaken TLS settings for other services.
6
+ PRODUCTION_SSL_CIPHERS = 'DEFAULT@SECLEVEL=1'.freeze
7
+
3
8
  WSDL = {
4
9
  development: 'https://wswhomo.afip.gov.ar/wsfev1/service.asmx?WSDL',
5
10
  production: 'https://servicios1.afip.gov.ar/wsfev1/service.asmx?WSDL',
@@ -13,7 +18,14 @@ module Afipws
13
18
  def initialize options = {}
14
19
  @cuit = options[:cuit]
15
20
  @wsaa = WSAA.new options.merge(service: 'wsfe')
16
- @client = Client.new Hash(options[:savon]).reverse_merge(wsdl: WSDL[@wsaa.env], convert_request_keys_to: :camelcase)
21
+ savon_options = Hash(options[:savon]).reverse_merge(
22
+ wsdl: WSDL[@wsaa.env],
23
+ convert_request_keys_to: :camelcase
24
+ )
25
+ if @wsaa.env == :production
26
+ savon_options[:ssl_ciphers] ||= RawSSLCipherString.new(PRODUCTION_SSL_CIPHERS)
27
+ end
28
+ @client = Client.new savon_options
17
29
  end
18
30
 
19
31
  def dummy
data/mise.toml ADDED
@@ -0,0 +1,14 @@
1
+ [tools]
2
+ ruby = "3.3.4"
3
+
4
+ [tasks.setup]
5
+ description = "Install Ruby dependencies"
6
+ run = "bundle install"
7
+
8
+ [tasks.test]
9
+ description = "Run the test suite"
10
+ run = "bundle exec rake"
11
+
12
+ [tasks.console]
13
+ description = "Open the project console"
14
+ run = "bundle exec ruby -Ilib bin/console"
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'date'
4
+ require 'afipws'
5
+
6
+ environment = (ENV['ARCA_ENV'] || 'development').to_sym
7
+
8
+ cuit = ENV.fetch('ARCA_CUIT')
9
+ key_path = ENV.fetch('ARCA_KEY', 'keys/arca-dev.key')
10
+ cert_path = ENV.fetch('ARCA_CERT', 'keys/arca-dev.crt')
11
+
12
+ options = {
13
+ env: environment,
14
+ cuit: cuit,
15
+ key: File.read(key_path),
16
+ cert: File.read(cert_path),
17
+ ta_path: "tmp/#{cuit}-#{environment}-smoke-wsaa-ta.dump"
18
+ }
19
+
20
+ failures = 0
21
+
22
+ def summarize(value)
23
+ case value
24
+ when Array
25
+ "#{value.size} records"
26
+ when Hash
27
+ "#{value.size} fields"
28
+ else
29
+ value.inspect
30
+ end
31
+ end
32
+
33
+ def run_check(name)
34
+ value = yield
35
+ puts "PASS #{name} (#{summarize(value)})"
36
+ rescue StandardError => e
37
+ message = e.message.gsub(/\s+/, ' ').strip
38
+ puts "FAIL #{name}: #{e.class}: #{message}"
39
+ false
40
+ end
41
+
42
+ def check(name)
43
+ result = yield
44
+ result == false ? 1 : 0
45
+ end
46
+
47
+ wsfe = Afipws::WSFE.new(options.merge(ta_path: "tmp/#{cuit}-#{environment}-smoke-wsfe-ta.dump"))
48
+ failures += check('WSFE dummy') { run_check('WSFE dummy') { wsfe.dummy } }
49
+ failures += check('WSFE cotizacion') { run_check('WSFE cotizacion DOL') { wsfe.cotizacion('DOL') } }
50
+ failures += check('WSFE tipos_comprobantes') { run_check('WSFE tipos_comprobantes') { wsfe.tipos_comprobantes } }
51
+ failures += check('WSFE tipos_documentos') { run_check('WSFE tipos_documentos') { wsfe.tipos_documentos } }
52
+ failures += check('WSFE tipos_concepto') { run_check('WSFE tipos_concepto') { wsfe.tipos_concepto } }
53
+ failures += check('WSFE tipos_monedas') { run_check('WSFE tipos_monedas') { wsfe.tipos_monedas } }
54
+ failures += check('WSFE tipos_iva') { run_check('WSFE tipos_iva') { wsfe.tipos_iva } }
55
+ failures += check('WSFE tipos_tributos') { run_check('WSFE tipos_tributos') { wsfe.tipos_tributos } }
56
+ failures += check('WSFE tipos_opcional') { run_check('WSFE tipos_opcional') { wsfe.tipos_opcional } }
57
+ failures += check('WSFE cant_max_registros_x_lote') { run_check('WSFE cant_max_registros_x_lote') { wsfe.cant_max_registros_x_lote } }
58
+
59
+ a100 = Afipws::PersonaServiceA100.new(options.merge(ta_path: "tmp/#{cuit}-#{environment}-smoke-a100-ta.dump"))
60
+ failures += check('PersonaServiceA100 dummy') { run_check('PersonaServiceA100 dummy') { a100.dummy } }
61
+ failures += check('PersonaServiceA100 jurisdictions') { run_check('PersonaServiceA100 jurisdictions') { a100.jurisdictions } }
62
+ failures += check('PersonaServiceA100 company_types') { run_check('PersonaServiceA100 company_types') { a100.company_types } }
63
+ failures += check('PersonaServiceA100 public_organisms') { run_check('PersonaServiceA100 public_organisms') { a100.public_organisms } }
64
+
65
+ if (persona_id = ENV['ARCA_PERSONA_ID'])
66
+ a4 = Afipws::PersonaServiceA4.new(options.merge(ta_path: "tmp/#{cuit}-#{environment}-smoke-a4-ta.dump"))
67
+ failures += check('PersonaServiceA4 get_persona') { run_check('PersonaServiceA4 get_persona') { a4.get_persona(persona_id) } }
68
+
69
+ a5 = Afipws::PersonaServiceA5.new(options.merge(ta_path: "tmp/#{cuit}-#{environment}-smoke-a5-ta.dump"))
70
+ failures += check('PersonaServiceA5 get_persona') { run_check('PersonaServiceA5 get_persona') { a5.get_persona(persona_id) } }
71
+
72
+ constancia = Afipws::WSConstanciaInscripcion.new(options.merge(ta_path: "tmp/#{cuit}-#{environment}-smoke-constancia-ta.dump"))
73
+ failures += check('WSConstanciaInscripcion get_persona') do
74
+ run_check('WSConstanciaInscripcion get_persona') { constancia.get_persona(persona_id) }
75
+ end
76
+ else
77
+ puts 'SKIP persona lookups (set ARCA_PERSONA_ID to run them)'
78
+ end
79
+
80
+ if ENV['ARCA_RUN_WCONS'] == '1'
81
+ wcons = Afipws::WConsDeclaracion.new(
82
+ **options.merge(
83
+ tipo_agente: ENV.fetch('ARCA_TIPO_AGENTE', 'IMEX'),
84
+ rol: ENV.fetch('ARCA_ROL', 'IMEX'),
85
+ ta_path: "tmp/#{cuit}-#{environment}-smoke-wcons-ta.dump"
86
+ )
87
+ )
88
+ failures += check('WConsDeclaracion dummy') { run_check('WConsDeclaracion dummy') { wcons.dummy } }
89
+ failures += check('WConsDeclaracion list') do
90
+ run_check('WConsDeclaracion list') do
91
+ wcons.detallada_lista_declaraciones(
92
+ fecha_oficializacion_desde: Date.new(2020, 1, 1),
93
+ fecha_oficializacion_hasta: Date.today
94
+ )
95
+ end
96
+ end
97
+ else
98
+ puts 'SKIP WConsDeclaracion (set ARCA_RUN_WCONS=1 to run it)'
99
+ end
100
+
101
+ abort "#{failures} smoke check(s) failed" unless failures.zero?
102
+ puts "Smoke test passed for #{environment}."
@@ -2,6 +2,12 @@ require 'spec_helper'
2
2
 
3
3
  module Afipws
4
4
  describe Client do
5
+ it 'preserva los modificadores de seguridad OpenSSL en cifrados explícitos' do
6
+ ssl = HTTPI::Auth::SSL.new
7
+ ssl.ciphers = RawSSLCipherString.new('DEFAULT@SECLEVEL=1')
8
+ ssl.ciphers.should == 'DEFAULT@SECLEVEL=1'
9
+ end
10
+
5
11
  context 'errores' do
6
12
  subject { Client.new(wsdl: Afipws::WSFE::WSDL[:test]) }
7
13
 
@@ -22,12 +22,6 @@ module Afipws
22
22
  end
23
23
  end
24
24
 
25
- context 'codificación del tra' do
26
- it 'debería quitarle el header y footer' do
27
- subject.codificar_tra(OpenSSL::PKCS7.new).should_not include 'BEGIN', 'END'
28
- end
29
- end
30
-
31
25
  context 'login' do
32
26
  it 'debería mandar el TRA al WS y obtener el TA' do
33
27
  ws = WSAA.new key: 'key', cert: 'cert'
@@ -278,9 +278,35 @@ module Afipws
278
278
 
279
279
  it 'debería usar las url para production cuando el env es production' do
280
280
  Client.expects(:new).with(wsdl: 'https://wsaa.afip.gov.ar/ws/services/LoginCms?wsdl')
281
- Client.expects(:new).with(wsdl: 'https://servicios1.afip.gov.ar/wsfev1/service.asmx?WSDL', convert_request_keys_to: :camelcase)
281
+ Client.expects(:new).with(
282
+ wsdl: 'https://servicios1.afip.gov.ar/wsfev1/service.asmx?WSDL',
283
+ convert_request_keys_to: :camelcase,
284
+ ssl_ciphers: WSFE::PRODUCTION_SSL_CIPHERS
285
+ )
282
286
  WSFE.new env: 'production'
283
287
  end
288
+
289
+ it 'permite reemplazar los cifrados de production explícitamente' do
290
+ Client.expects(:new).with(
291
+ wsdl: 'https://wsaa.afip.gov.ar/ws/services/LoginCms?wsdl',
292
+ ssl_ciphers: 'DEFAULT'
293
+ )
294
+ Client.expects(:new).with(
295
+ wsdl: 'https://servicios1.afip.gov.ar/wsfev1/service.asmx?WSDL',
296
+ convert_request_keys_to: :camelcase,
297
+ ssl_ciphers: 'DEFAULT'
298
+ )
299
+ WSFE.new env: :production, savon: {ssl_ciphers: 'DEFAULT'}
300
+ end
301
+
302
+ it 'no debilita los cifrados fuera de production' do
303
+ Client.expects(:new).with(wsdl: 'https://wsaahomo.afip.gov.ar/ws/services/LoginCms?wsdl')
304
+ Client.expects(:new).with(
305
+ wsdl: 'https://wswhomo.afip.gov.ar/wsfev1/service.asmx?WSDL',
306
+ convert_request_keys_to: :camelcase
307
+ )
308
+ WSFE.new env: :development
309
+ end
284
310
  end
285
311
 
286
312
  context 'manejo de errores' do
@@ -2,7 +2,7 @@
2
2
  &lt;loginTicketResponse version=&quot;1&quot;&gt;
3
3
  &lt;header&gt;
4
4
  &lt;source&gt;CN=wsaahomo, O=AFIP, C=AR, SERIALNUMBER=CUIT 33693450239&lt;/source&gt;
5
- &lt;destination&gt;C=ar, O=nicolau emmanuel, SERIALNUMBER=CUIT 20300032673, CN=vitolendev&lt;/destination&gt;
5
+ &lt;destination&gt;C=ar, O=john doe, SERIALNUMBER=CUIT 20123456783, CN=acme&lt;/destination&gt;
6
6
  &lt;uniqueId&gt;1318562072&lt;/uniqueId&gt;
7
7
  &lt;generationTime&gt;2011-01-12T18:57:04.0-03:00&lt;/generationTime&gt;
8
8
  &lt;expirationTime&gt;2011-01-13T06:57:04.0-03:00&lt;/expirationTime&gt;
@@ -3,7 +3,7 @@
3
3
  <FECAESolicitarResponse xmlns="http://ar.gov.afip.dif.FEV1/">
4
4
  <FECAESolicitarResult>
5
5
  <FeCabResp>
6
- <Cuit>20300032673</Cuit>
6
+ <Cuit>20123456783</Cuit>
7
7
  <PtoVta>1</PtoVta>
8
8
  <CbteTipo>1</CbteTipo>
9
9
  <FchProceso>20110113</FchProceso>
@@ -3,7 +3,7 @@
3
3
  <FECAESolicitarResponse xmlns="http://ar.gov.afip.dif.FEV1/">
4
4
  <FECAESolicitarResult>
5
5
  <FeCabResp>
6
- <Cuit>20300032673</Cuit>
6
+ <Cuit>20123456783</Cuit>
7
7
  <PtoVta>1</PtoVta>
8
8
  <CbteTipo>1</CbteTipo>
9
9
  <FchProceso>20110115</FchProceso>
@@ -3,7 +3,7 @@
3
3
  <FECAESolicitarResponse xmlns="http://ar.gov.afip.dif.FEV1/">
4
4
  <FECAESolicitarResult>
5
5
  <FeCabResp>
6
- <Cuit>20300032673</Cuit>
6
+ <Cuit>20123456783</Cuit>
7
7
  <PtoVta>1</PtoVta>
8
8
  <CbteTipo>1</CbteTipo>
9
9
  <FchProceso>20110114</FchProceso>
@@ -3,7 +3,7 @@
3
3
  <FECAESolicitarResponse xmlns="http://ar.gov.afip.dif.FEV1/">
4
4
  <FECAESolicitarResult>
5
5
  <FeCabResp>
6
- <Cuit>20300032673</Cuit>
6
+ <Cuit>20123456783</Cuit>
7
7
  <PtoVta>1</PtoVta>
8
8
  <CbteTipo>1</CbteTipo>
9
9
  <FchProceso>20110114</FchProceso>
@@ -3,7 +3,7 @@
3
3
  <FECAEARegInformativoResponse xmlns="http://ar.gov.afip.dif.FEV1/">
4
4
  <FECAEARegInformativoResult>
5
5
  <FeCabResp>
6
- <Cuit>20300032673</Cuit>
6
+ <Cuit>20123456783</Cuit>
7
7
  <PtoVta>3</PtoVta>
8
8
  <CbteTipo>1</CbteTipo>
9
9
  <FchProceso>20110127</FchProceso>
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: afipws
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.0
4
+ version: 2.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emmanuel Nicolau
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 1980-01-01 00:00:00.000000000 Z
11
+ date: 2026-07-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -52,20 +52,6 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
- - !ruby/object:Gem::Dependency
56
- name: rubocop
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - "~>"
60
- - !ruby/object:Gem::Version
61
- version: 0.80.0
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - "~>"
67
- - !ruby/object:Gem::Version
68
- version: 0.80.0
69
55
  - !ruby/object:Gem::Dependency
70
56
  name: pry
71
57
  requirement: !ruby/object:Gem::Requirement
@@ -100,14 +86,14 @@ dependencies:
100
86
  requirements:
101
87
  - - "~>"
102
88
  - !ruby/object:Gem::Version
103
- version: 2.12.0
89
+ version: 2.15.0
104
90
  type: :runtime
105
91
  prerelease: false
106
92
  version_requirements: !ruby/object:Gem::Requirement
107
93
  requirements:
108
94
  - - "~>"
109
95
  - !ruby/object:Gem::Version
110
- version: 2.12.0
96
+ version: 2.15.0
111
97
  - !ruby/object:Gem::Dependency
112
98
  name: httpclient
113
99
  requirement: !ruby/object:Gem::Requirement
@@ -158,12 +144,12 @@ executables:
158
144
  extensions: []
159
145
  extra_rdoc_files: []
160
146
  files:
161
- - ".envrc"
162
147
  - ".github/workflows/ci.yml"
163
148
  - ".gitignore"
164
149
  - ".rspec"
165
150
  - ".rubocop.yml"
166
151
  - ".ruby-version"
152
+ - AGENTS.md
167
153
  - CHANGELOG.md
168
154
  - Gemfile
169
155
  - Gemfile.lock
@@ -189,7 +175,8 @@ files:
189
175
  - lib/afipws/ws_constancia_inscripcion.rb
190
176
  - lib/afipws/wsaa.rb
191
177
  - lib/afipws/wsfe.rb
192
- - shell.nix
178
+ - mise.toml
179
+ - script/smoke_test.rb
193
180
  - spec/afipws/client_spec.rb
194
181
  - spec/afipws/core_ext/hash_spec.rb
195
182
  - spec/afipws/persona_service_a100_spec.rb
@@ -255,8 +242,6 @@ files:
255
242
  - spec/fixtures/wsfe/fecaea_solicitar/error_distinto.xml
256
243
  - spec/fixtures/wsfe/fecaea_solicitar/success.xml
257
244
  - spec/fixtures/wsfe/wsfe.wsdl
258
- - spec/manual/generar_keys.txt
259
- - spec/manual/test.rb
260
245
  - spec/spec_helper.rb
261
246
  - spec/support/fake_wsaa.rb
262
247
  - spec/support/matchers.rb
@@ -279,7 +264,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
279
264
  - !ruby/object:Gem::Version
280
265
  version: '0'
281
266
  requirements: []
282
- rubygems_version: 3.2.26
267
+ rubygems_version: 3.5.11
283
268
  signing_key:
284
269
  specification_version: 4
285
270
  summary: Ruby client para los web services de la AFIP
@@ -349,8 +334,6 @@ test_files:
349
334
  - spec/fixtures/wsfe/fecaea_solicitar/error_distinto.xml
350
335
  - spec/fixtures/wsfe/fecaea_solicitar/success.xml
351
336
  - spec/fixtures/wsfe/wsfe.wsdl
352
- - spec/manual/generar_keys.txt
353
- - spec/manual/test.rb
354
337
  - spec/spec_helper.rb
355
338
  - spec/support/fake_wsaa.rb
356
339
  - spec/support/matchers.rb
data/.envrc DELETED
@@ -1,2 +0,0 @@
1
- layout ruby
2
- use nix
data/shell.nix DELETED
@@ -1,6 +0,0 @@
1
- with import <nixpkgs> {};
2
- mkShell {
3
- nativeBuildInputs = [
4
- ruby
5
- ];
6
- }
@@ -1,8 +0,0 @@
1
- # Generar clave privada
2
- openssl genrsa -out test.key 1024
3
-
4
- # Generar el CSR para enviar a la AFIP
5
- openssl req -new -key test.key -subj "/C=AR/O=[empresa]/CN=[nombre]/serialNumber=CUIT [nro_cuit]" -out test.csr
6
-
7
- # Genera un certificado firmado por nosotros, sería el paso que tiene que hacer la AFIP en base al CSR
8
- openssl req -x509 -days 1095 -out test.crt -verify -key test.key -sha1 -new -batch
data/spec/manual/test.rb DELETED
@@ -1,35 +0,0 @@
1
- $LOAD_PATH.unshift(File.expand_path('lib')); require 'afipws'
2
-
3
- ws = Afipws::WSFE.new env: :development, cuit: '20300032673', cert: File.read('spec/manual/test.crt'), key: File.read('spec/manual/test.key'), savon: {log: true}
4
-
5
- def obtener_ta ws
6
- ws.cotizacion 'DOL'
7
- xml = Builder::XmlMarkup.new indent: 2
8
- xml.ar :Auth do
9
- xml.ar :Token, ws.ta[:token]
10
- xml.ar :Sign, ws.ta[:sign]
11
- xml.ar :Cuit, ws.cuit
12
- end
13
- puts xml.target!
14
- end
15
-
16
- def autorizar_comprobante ws
17
- ultimo = ws.ultimo_comprobante_autorizado pto_vta: 1, cbte_tipo: 1
18
- # cant_informar = ws.cant_max_registros_x_lote
19
- cant_informar = 2
20
- puts "Informando #{cant_informar} comprobantes"
21
- comprobantes = (1..cant_informar).to_a.map do |i|
22
- {
23
- cbte_nro: ultimo + i, concepto: 1, doc_nro: 30521189203, doc_tipo: 80, cbte_fch: Date.today,
24
- imp_total: 1270.48, imp_neto: 1049.98, imp_iva: 220.50, mon_id: 'PES', mon_cotiz: 1,
25
- iva: { alic_iva: [{ id: 5, base_imp: 1049.98, importe: 220.50 }]}
26
- }
27
- end
28
- puts ws.autorizar_comprobantes(cbte_tipo: 1, pto_vta: 1, comprobantes: comprobantes)
29
- end
30
-
31
- def consultar_caea ws
32
- ws.consultar_caea Date.new(2011,2,3)
33
- end
34
-
35
- obtener_ta ws