snoopy_afip 4.3.0 → 4.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.
data/README.md CHANGED
@@ -1,238 +1,136 @@
1
1
  # snoopy_afip
2
- conexión con Web Service de Factura Electrónica de AFIP (WSFE)
3
2
 
4
- ## Instalación
5
-
6
- Añadir esta linea en el Gemfile:
7
-
8
- ```ruby
9
- gem 'snoopy_afip'
10
- ```
11
-
12
- Luego ejecuta:
13
-
14
- $ bundle
15
-
16
- O instala la gema a mano:
17
-
18
- $ gem install snoopy_afip
19
-
20
- ### Versiones
21
-
22
- - `2.1.1`. Usada en producción. (**Stable**)
23
- - `3.0.0`. Aun en entorono de testing. (**Beta**)
24
-
25
- ## Antes que nada
26
-
27
- * Link con el manual para desarrolladores.
3
+ Gema Ruby que adapta la **Facturación Electrónica de AFIP (Argentina)**. Expone el módulo `Snoopy` y resuelve, vía SOAP (`savon`), los dos servicios de AFIP necesarios para emitir comprobantes electrónicos:
28
4
 
29
- - [Especificación Técnica WSAA]('http://www.afip.gov.ar/ws/WSAA/Especificacion_Tecnica_WSAA_1.2.0.pdf'): Especificación técnica para la comunicación con el **WSAA** (servicio de autenticación), su propósito es solicitar un _tocken_ y _sign_ para poder emitir facturas con el servicio de **WSFE** (Servicio de autorización de facturas)
30
-
31
- - [Manual desarrollador](http://www.afip.gob.ar/fe/documentos/manual_desarrollador_COMPG_v2_9.pdf): Especificación técnica para la comunicación con **WSFE** (Servicio de autorización de facturas).
32
-
33
- * Es recomendable descargar los **Wsdl** para evitar tener que pedirlo en cada request
34
-
35
- - Wsdl **WSAA** (servicio de autenticación)
36
-
37
- - Testing: [https://wsaahomo.afip.gov.ar/ws/services/LoginCms?wsdl]('https://wsaahomo.afip.gov.ar/ws/services/LoginCms?wsdl')
38
- - Producción: [https://wsaa.afip.gov.ar/ws/services/LoginCms?wsdl]('https://wsaa.afip.gov.ar/ws/services/LoginCms?wsdl')
39
-
40
- - **WSFE** (Servicio de autorización de facturas)
41
-
42
- - Testing: [https://wswhomo.afip.gov.ar/wsfev1/service.asmx?wsdl]('https://wswhomo.afip.gov.ar/wsfev1/service.asmx?wsdl')
43
- - Producción: [https://servicios1.afip.gov.ar/wsfev1/service.asmx?WSDL]('https://servicios1.afip.gov.ar/wsfev1/service.asmx?WSDL')
44
-
45
- * Explicación detallada de los pasos a seguir para obtener el certificado desde el sitio web AFIP para emitir facturas electrónicas.
46
-
47
- [obtención de certificado](https://www.afip.gob.ar/ws/WSAA/WSAA.ObtenerCertificado.pdf)
48
-
49
- ## USO
50
-
51
- ### Inicialización de parametros generales
52
-
53
- ```ruby
54
- Snoopy.default_currency = :peso
55
- Snoopy.default_concept = 'Servicios'
56
- Snoopy.default_document_type = 'CUIT' || 'DNI' # O alguna key de Snoopy::DOCUMENTS
57
- # Para el caso de produccion
58
- Snoopy.auth_url = 'https://wsaa.afip.gov.ar/ws/services/LoginCms?wsdl' || PATH_WSAA_PROP_WSDL
59
- Snoopy.service_url = 'https://servicios1.afip.gov.ar/wsfev1/service.asmx?WSDL' || PATH_WSFE_PROP_WSDL
60
- # Para el caso de desarrollo
61
- Snoopy.auth_url = 'https://wsaahomo.afip.gov.ar/ws/services/LoginCms?wsdl' || PATH_WSAA_TEST_WSDL
62
- Snoopy.service_url = 'https://wswhomo.afip.gov.ar/wsfev1/service.asmx?wsdl' || PATH_WSFE_TEST_WSDL
63
- ```
5
+ - **WSAA** (autenticación): obtiene `token`/`sign` a partir de clave privada + certificado.
6
+ - **WSFE** (facturación electrónica v1): autoriza facturas y notas de crédito, devuelve el CAE.
64
7
 
65
- En caso de trabajar con `Ruby on Rails`, es recomendable crear un archivo con esta conf en `config/initializers/snoopy.rb`.
8
+ Versión actual: `4.3.0`.
66
9
 
67
- ### Generar Clave privada
10
+ ## Instalación
68
11
 
69
12
  ```ruby
70
- Snoopy::AuthenticationAdapter.generate_pkey(2048) # Si no se pasa argumento se generar una de 8192
13
+ # Gemfile
14
+ gem 'snoopy_afip'
71
15
  ```
72
16
 
73
- Este metodo retorna el `RAW` de la pkey, la cual debera guardarse en algun archivo o alguna base de datos.
74
-
75
- ### Generar solicitud de pedido de certificado
76
-
77
- #### With ruby
78
-
79
- Este metodo aun no ha sido testeado, por lo que agredezco si alguien logra probarlo.
80
-
81
- ```ruby
82
- Snoopy::AuthenticationAdapter.generate_certificate_request_with_ruby(pkey_path, subj_o, subj_cn, subj_cuit)
17
+ ```bash
18
+ bundle install
19
+ # o
20
+ gem install snoopy_afip
83
21
  ```
84
22
 
85
- #### With bash
86
-
87
- Mantiene la generación de versiones pasadas de `Snoopy`
23
+ ## Setup (desarrollo)
88
24
 
89
25
  ```bash
90
- Snoopy::AuthenticationAdapter.generate_certificate_request_with_bash(pkey_path, subj_o, subj_cn, subj_cuit)
26
+ bundle install
27
+ bundle exec rspec # ver estado de la suite en docs/test/testing.md
91
28
  ```
92
29
 
93
- - `pkey_path`: Ruta absoluta de la llave privada.
94
- - `subj_o`: Nombre de la compañia.
95
- - `subj_cn`: Hostname del server que generará las solicitudes. En ruby se obtiene con `%x(hostname).chomp`
96
- - `subj_cuit`: Cuit registrado en la AFIP de la compañia que emita facturas.
97
-
98
- Una vez generado este archivo debe hacerse el tramite en el sitio web de la **AFIP** para obtener el certificado que permitirá autorizar facturas al webservice.
99
-
100
- ### Solicitar autorización para la emisión de facturas
101
-
102
- Para poder emitir o autorizar facturas en el web service de la **AFIP** es necesario solicitar autorización.
103
-
104
- ```ruby
105
- # pkey_path: Ruta absoluta de la llave privada.
106
- # cert_path: Ruta absoluta del cetificado obtendio del sitio oficial de la **AFIP**.
107
- authentication_adapter = Snoopy::AuthenticationAdapter.new(pkey_path, pkey_cert)
108
- authentication_adapter.authenticate!
109
- ```
30
+ Dependencia de runtime: `savon ~> 2.12.1`. Gestión de Ruby con **chruby**, dependencias con **Bundler**.
110
31
 
111
- `authentication_adapter.authenticate!` deberá retornar un `Hash` con las siguientes keys: `:token`, `:sign` y `:expiration_time`
32
+ ## Configuración
112
33
 
113
- La key `:expiration_time` determina hasta cuando se podrá autorizar facturas, el tiempo esta prefijado por **AFIP** y es el de 24 Horas desde el momento de la solicitud del **token sign**, superado este tiempo deberá solicitarse nuevamente un **token sign**.
34
+ La gema se configura en el host (típicamente `config/initializers/snoopy.rb`):
114
35
 
115
36
  ```ruby
116
- # Response example
117
- { token: "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9InllcyI/Pgo8c3NvIHZlcnNpb249IjIuMCI+CiAgICA8aWQgdW5pcXVlX2lkPSI5MDIxNDczOTEiIHNyYz0iQ049d3NhYWhvbW8sIE89QUZJUCwgQz1BUiwgU0VSSUFMTlVNQkVSPUNVSVQgMzM2OTM0NTAyMzkiIGdlbl90aW1lPSIxNDk4NDk0NDQ1IiBleHBfdGltZT0iMTQ5ODUzNzcwNSIgZHN0PSJDTj13c2ZlLCBPPUFGSVAsIEM9QVIiLz4KICAgIDxvcGVyYXRpb24gdmFsdWU9ImdyYW50ZWQiIHR5cGU9ImxvZ2luIj4KICAgICAgICA8bG9naW4gdWlkPSJDPWFyLCBPPXNlcXVyZSBzYSwgU0VSSUFMTlVNQkVSPUNVSVQgMjAyNDE2MDgxNjcsIENOPXBjIiBzZXJ2aWNlPSJ3c2ZlIiByZWdtZXRob2Q9IjIyIiBlbnRpdHk9IjMzNjkzNDUwMjM5IiBhdXRobWV0aG9kPSJjbXMiPgogICAgICAgICAgICA8cmVsYXRpb25zPgogICAgICAgICAgICAgICAgPHJlbGF0aW9uIHJlbHR5cGU9IjQiIGtleT0iMjAyNDE2MDgxNjciLz4KICAgICAgICAgICAgPC9yZWxhdGlvbnM+CiAgICAgICAgPC9sb2dpbj4KICAgIDwvb3BlcmF0aW9uPgo8L3Nzbz4KCg==",
118
- sign: "iSpp/5qxQntuzOQcqs6GlShFaOKtEagLY17TFDwMTiErquT/fEw5ki9Ff4RYGndc/49UGmUTnVjUqB0mxuJk2IG4t+J4AAyVsY6+xiBGvMXAM/5sAI78NDl7ibMxAcdPi+nBIrdydp5DLy2SB4u/G46kguc6+srBp2fo20f/+wM=",
119
- expiration_time: #<DateTime: 2017-06-27T01:28:25-03:00 ((2457932j,16105s,963000000n),-10800s,2299161j)>}}
37
+ Snoopy.default_currency = :peso
38
+ Snoopy.default_concept = 'Servicios'
39
+ Snoopy.default_document_type = 'CUIT' # alguna key de Snoopy::DOCUMENTS
40
+
41
+ # Homologación (testing)
42
+ Snoopy.auth_url = 'https://wsaahomo.afip.gov.ar/ws/services/LoginCms?wsdl'
43
+ Snoopy.service_url = 'https://wswhomo.afip.gov.ar/wsfev1/service.asmx?wsdl'
44
+ # Producción
45
+ # Snoopy.auth_url = 'https://wsaa.afip.gov.ar/ws/services/LoginCms?wsdl'
46
+ # Snoopy.service_url = 'https://servicios1.afip.gov.ar/wsfev1/service.asmx?WSDL'
47
+
48
+ Snoopy.pkey = '<path-o-PEM-de-la-clave-privada>' # secreto — nunca commitear
49
+ Snoopy.cert = '<path-o-PEM-del-certificado-AFIP>' # secreto — nunca commitear
50
+ Snoopy.cuit = '<CUIT-del-emisor>'
120
51
  ```
121
52
 
122
- En caso de producirse algun tipo de error este será devuelto a traves de raise exception. El error mas comun puede deberse al que certificado obtenido a treves del pedido de cetificado no sea correcto.
123
-
124
- ### Autorizar factura
125
-
126
- #### Crear `Bill`
127
- ```ruby
128
- Snoopy::Bill.new(attrs)
129
-
130
- ```
131
- Donde `attrs` debe estar conformado de la siguiente manera:
132
- * `attrs[:cuit]` CUIT del emisor de la factura.
133
- * `attrs[:concept]` Concepto de la factura, por defecto `Snoopy.default_concept`.
134
- * `attrs[:imp_iva]` Monto total de impuestos.
135
- * `attrs[:currency]` Tipo de moneda a utilizar, por default `Snoopy.default_currency`.
136
- * `attrs[:alicivas]` Impuestos asociados a la factura.
137
- * `attrs[:total_net]` Monto neto por defecto es 0.
138
- * `attrs[:sale_point]` Punto de venta del emisor de la factura.
139
- * `attrs[:document_type]` Tipo de documento a utilizar, por default `Snoopy.default_document`. Valores posibles `Snoopy::DOCUMENTS`
140
- * `attrs[:document_num]` Numero de documento o cuit, el valor depende de `attrs[:document_type]`.
141
- * `attrs[:issuer_iva_cond]` Condición de IVA del emisor de la factura. [`Snoopy::RESPONSABLE_INSCRIPTO` o `Snoopy::RESPONSABLE_MONOTRIBUTO`]
142
- * `attrs[:receiver_iva_cond]` valores posibles `Snoopy::BILL_TYPE`
143
- * `attrs[:service_date_from]` Inicio de vigencia de la factura.
144
- * `attrs[:service_date_to]` Fin de vigencia de la factura.
145
- * `attrs[:cbte_asoc_num]` Numero de la factura a la cual se quiere crear una nota de crédito (Solo pasar si se esta creando una nota de crédito).
146
- * `attrs[:cbte_asoc_to_sale_point]` Punto de venta de la factura a la cual se quiere crear una nota de crédito (Solo pasar si se esta creando una nota de crédito).
147
-
148
- El `attrs[:alicivas]` discrimina la información de los items. Es posible que en la factura se discriminen diferentes items con diferentes impuestos. Para ello el `attrs[:alicivas]` es un arreglo de `Hash`. Donde cada uno de ellos tiene la información sobre un determinado impuesto.
53
+ > El inventario completo de opciones (tipos, defaults, secretos, failure-mode) está en [`docs/config/configuracion.md`](docs/config/configuracion.md). Nunca incrustar CUIT, claves ni certificados reales en el código ni en el repo.
149
54
 
150
- ```ruby
151
- { id: tax_rate.round_with_precision(2), # Porcentaje. Ej: "0.105", "0.21", "0.27"
152
- amount: tax_amount.round_with_precision(2), # Monto total del impuesto del item.
153
- taxeable_base: net_amount.round_with_precision(2) } # Monto total del item sin considerar el impuesto.
154
- ```
55
+ ## Uso
155
56
 
156
- Por ejemplo si se tiene 5 items, donde 3 de ellos tienen un impuesto del 10.5% y los 2 restantes del 21%. Los primeros 3 deben, se debera de crear dos hashes de la siguientes forma.
57
+ ### 1. Autenticar (WSAA)
157
58
 
158
59
  ```ruby
159
- attrs[:alicivas] = [ { id: (10.5 / 100 ).to_s
160
- amount: X, # De los 3 items de 10.5
161
- taxeable_base: Y }, # De los 3 items de 10.5
162
- { id: (21 / 100 ).to_s
163
- amount: X, # De los 2 items de 21
164
- taxeable_base: Y } ] # De los 2 items de 21
60
+ auth = Snoopy::AuthenticationAdapter.new(pkey: pkey_path, cert: cert_path)
61
+ credentials = auth.authenticate!
62
+ # => { token: "<token>", sign: "<sign>", expiration_time: <DateTime> } (válido 12h)
165
63
  ```
166
- Donde:
167
64
 
168
- * `X`: Es la parte del monto que le corresponde al impuesto.
169
- * `Y`: Es la partes del monto sin impuesto.
65
+ Helpers para generar clave/CSR: `Snoopy::AuthenticationAdapter.generate_pkey`, `.generate_certificate_request_with_ruby`, `.generate_certificate_request_with_bash`. Trámite del certificado: [AFIP Obtener certificado](https://www.afip.gob.ar/ws/WSAA/WSAA.ObtenerCertificado.pdf).
170
66
 
171
- Los `tax_rates` soportados por AFIP son los siguientes:
67
+ ### 2. Crear el comprobante (`Bill`)
172
68
 
173
69
  ```ruby
174
- Snoopy::ALIC_IVA
70
+ bill = Snoopy::Bill.new(
71
+ cuit: cuit,
72
+ sale_point: sale_point,
73
+ concept: 'Servicios',
74
+ document_type: 'CUIT',
75
+ document_num: '30710151543',
76
+ issuer_iva_cond: Snoopy::RESPONSABLE_INSCRIPTO,
77
+ receiver_iva_cond: :factura_a, # key de Snoopy::BILL_TYPE → cbte_type
78
+ receiver_iva_condition: :responsable_inscripto,
79
+ total_net: 1000.0,
80
+ alicivas: [ { id: 0.21, amount: 210.0, taxeable_base: 1000.0 } ]
81
+ )
82
+ bill.valid? # corre validaciones → bill.errors
175
83
  ```
176
84
 
177
- * Tips:
85
+ `alicivas` discrimina el IVA por ítem (`id` = porcentaje, `amount` = monto del impuesto, `taxeable_base` = neto sin IVA). Tasas soportadas: `Snoopy::ALIC_IVA`. Detalle de términos en [`docs/glossary/glossary.md`](docs/glossary/glossary.md).
178
86
 
179
- El `taxeable_base` se calcula de la siguiente manera:
87
+ ### 3. Autorizar (WSFE)
180
88
 
181
89
  ```ruby
182
- total_amount / (1 + tax_percentage)
183
- ```
184
-
185
- - Metodos de interes
186
- - **valid?** valida si la `bill` cumple lo minimo indispensable.
187
- - **partial_approved?** si fue _Aprobada parcialmente_ por el webserice.
188
- - **rejected?** si fue _Rechazada_ por el webservice.
189
- - **approved?** si fue _Aprobada_ por el webservice.
190
- - **errors** errores presentados durante la validación `valid?`
191
-
192
- #### Autorizar el `Bill`
193
- ```ruby
194
- authorize_adapter = Snoopy::AuthorizeAdapter.new({ bill: bill, # Obtenido en el paso anterior
195
- pkey: pkey, # PATH de la clave privada generada anteriormente.
196
- cert: certificate, # PATH del certificado descargado del sitio web de la AFIP.
197
- cuit: cuit, # CUIT del emisor de la factura, mismo con el que se hizo el tramite.
198
- sign: sign, # SIGN obtenido en la autenticación
199
- token: token}) # TOKEN obtenido en la autenticación
200
-
201
- authorize_adapter.set_bill_number!
202
-
203
- authorize_adapter.authorize!
90
+ adapter = Snoopy::AuthorizeAdapter.new(
91
+ bill: bill,
92
+ pkey: pkey, cert: cert, cuit: cuit,
93
+ token: credentials[:token], sign: credentials[:sign]
94
+ )
95
+ adapter.set_bill_number! # numera con el último autorizado + 1
96
+ adapter.authorize! # => true/false; setea bill.cae, bill.result, ...
204
97
  ```
205
98
 
206
- - Metodos de interes del `Snoopy::AuthorizeAdapter`
99
+ Lecturas de la respuesta: `adapter.afip_errors`, `adapter.afip_events`, `adapter.afip_observations`, `adapter.errors`, `adapter.response`. Estado del comprobante: `bill.approved?`, `bill.partial_approved?`, `bill.rejected?`.
207
100
 
208
- - **request** Información enviada al webservice de **AFIP**
209
- - **response** Respuesta completa obtenida del webservice de **AFIP**
210
- - **afip_errors** Errores parseados de la **response**. Pueden presentarse los motivos por los que no se autorizo la `bill`
211
- - **afip_events** Eventos parseados de la **response** . Generalmente la **AFIP** los usa para informar de posibles cambios.
212
- - **afip_observations** Observaciones parseados de la **response**. Pueden presentarse los motivos por los que no se autorizo la `bill`
213
- - **errors** Errores presentados en el proceso de autorización. Explicado en el apartado siguiente.
101
+ ### Manejo de errores
214
102
 
215
- ### Manejo de excepciones
103
+ - **Explota** (`raise`): fallo de comunicación con AFIP (`Snoopy::Exception::ServerTimeout`, `ClientError`) → no se autorizó el comprobante.
104
+ - **No explota**: AFIP respondió pero el parseo de la respuesta falló o devolvió errores de negocio → se acumulan en `adapter.errors` / `afip_errors` (parseo en 4 capas independientes para que un cambio de formato de AFIP no tumbe todo).
216
105
 
217
- #### Explota
106
+ Catálogo y política completos en [`docs/errors/errors.md`](docs/errors/errors.md).
218
107
 
219
- Hay errores que producirán que `Snoopy` generé un `raise`, estos son los casos que se produzcan errores previo o durante la comunicación con la **AFIP**. En estos casos se produce un `raise` debido a que no se logro la comunicación con la **AFIP** por lo que se puede asegurar que no se autorizo la `bill`
108
+ ## Índice de artefactos (`docs/`)
220
109
 
221
- #### No Explota
222
- Existe otro caso que se produzca un error posterior a la comunicación con el webservice, en este caso se pudo obtener una respuesta , pero se producierón errores en el parseo de la misma (Por ejemplo **AFIP** cambia el formato de la response), para ellos se puede consultar las exceptiones generadas por los parseadores con `authorize_adapter.errors`.
110
+ | capa | artefacto | contenido |
111
+ |---|---|---|
112
+ | interfaz | [`docs/interface/interface.md`](docs/interface/interface.md) | API Ruby pública (`Snoopy`, adapters, `Bill`, constantes) |
113
+ | comportamiento | [`docs/behavior/behavior.md`](docs/behavior/behavior.md) | secuencias WSAA / WSFE |
114
+ | glosario | [`docs/glossary/glossary.md`](docs/glossary/glossary.md) | términos AFIP (CAE, cbte_type, alicivas, IVA cond) |
115
+ | consumidas | [`docs/consumed/afip.md`](docs/consumed/afip.md) | contrato SOAP consumido (WSAA + WSFE) |
116
+ | errores | [`docs/errors/errors.md`](docs/errors/errors.md) | jerarquía `Snoopy::Exception::*` + política |
117
+ | configuración | [`docs/config/configuracion.md`](docs/config/configuracion.md) | inventario de opciones runtime |
118
+ | topología | [`docs/topology/topology.md`](docs/topology/topology.md) | dependencias + servicios externos |
119
+ | test | [`docs/test/testing.md`](docs/test/testing.md) | suite, gaps de cobertura |
120
+ | operaciones (api) | n/a | gema sin superficie HTTP/CLI propia |
121
+ | datos | n/a | sin base de datos |
122
+ | eventos · multi-tenancy | n/a | no aplica |
223
123
 
224
- Hay 4 nivel de parser:
225
- - **Obtención del resultado, cae, fecha de vencimiento del cae y el bill number**: Este es el nivel de parser mas **importante** dado que si este no se pudo realizar, es imposible saber que sucedio durante la autorización. Pero no preocuparse por que esta la response completa en `authorize_adapter.response`
226
- - **Parseo de errores**: Parsea los errores retornados por la **AFIP**.
227
- - **Parseo de eventos**: Parsea los eventos retornados por la **AFIP**.
228
- - **Parseo de observaciones**: Parsea las observaciones retornados por la **AFIP**.
124
+ ## Referencias AFIP
229
125
 
230
- Implemente esto de esta manera debido a que sucedio que **AFIP** cambio algo en la estructura de los eventos, y al tener todo en un solo parser explotaba todo y no sabia que habia sucedido con la autorización de la factura.
126
+ - [Especificación Técnica WSAA 1.2.0](http://www.afip.gov.ar/ws/WSAA/Especificacion_Tecnica_WSAA_1.2.0.pdf)
127
+ - [Manual del desarrollador WSFE](http://www.afip.gob.ar/fe/documentos/manual_desarrollador_COMPG_v2_9.pdf)
231
128
 
232
129
  ## TO DO
233
- - **Mejor parseo de los errores obtenidos en AFIP**: La idea seria poder identificar cada uno de los errores posibles obtenido, y agregar los errores en la `bill` al atributo que corresponda.
234
- - **Batch**: Permitir a `Snoopy::AuthorizeAdapter` autorizar un pool de `Snoopy::Bill` en una sola request.
130
+
131
+ - Mejor parseo de los errores de AFIP (mapear cada código al atributo del `Bill`).
132
+ - Batch: autorizar un pool de `Bill` en una sola request.
235
133
 
236
134
  ## License
237
135
 
238
- The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
136
+ MIT ver [LICENSE.txt](LICENSE.txt).
@@ -0,0 +1,84 @@
1
+ # Comportamiento — snoopy_afip
2
+ > meta: artefacto · RFC-007 · generado arch-enrich · anclado a 7813cf2 · cobertura: 3 flujos documentados / 0 pendientes localizados
3
+
4
+ ## Cobertura
5
+
6
+ | flujo | estado |
7
+ |---|---|
8
+ | Autenticación WSAA (`authenticate!`) | documentado |
9
+ | Numeración del comprobante (`set_bill_number!`) | documentado |
10
+ | Autorización WSFE (`authorize!`) | documentado |
11
+ | Consulta de comprobante (`invoice_informed?`) | no documentado (variante de lectura de `fe_comp_consultar`) |
12
+
13
+ ## 1. Resumen
14
+
15
+ Dos flujos de negocio encadenados: autenticación (WSAA, una vez por 12h) y autorización (WSFE, por comprobante). El segundo asume credenciales ya obtenidas. El diseño separa el parseo de la respuesta WSFE en capas que no se tumban entre sí.
16
+
17
+ ## 2. Flujos
18
+
19
+ ### Autenticación WSAA — `authenticate!`
20
+
21
+ Contexto: obtiene `token`/`sign` firmando un TRA con cert+pkey.
22
+
23
+ ```mermaid
24
+ sequenceDiagram
25
+ participant Host
26
+ participant Auth as AuthenticationAdapter
27
+ participant Cli as Client
28
+ participant WSAA as AFIP WSAA
29
+ Host->>Auth: new(pkey, cert)
30
+ Host->>Auth: authenticate!
31
+ Auth->>Auth: build_tra (XML loginTicketRequest)
32
+ Auth->>Auth: build_cms (PKCS7 sign cert+pkey)
33
+ Auth->>Cli: call(:login_cms, in0 cms)
34
+ Cli->>WSAA: SOAP login_cms
35
+ WSAA-->>Cli: loginTicketResponse
36
+ Cli-->>Auth: body
37
+ Auth->>Auth: Nori.parse + extrae credentials
38
+ Auth-->>Host: {token, sign, expiration_time}
39
+ ```
40
+
41
+ ### Autorización WSFE — `set_bill_number!` + `authorize!`
42
+
43
+ Contexto: numera y autoriza un `Bill` válido; setea CAE y parsea errores/eventos/observaciones en capas independientes.
44
+
45
+ ```mermaid
46
+ sequenceDiagram
47
+ participant Host
48
+ participant Adapter as AuthorizeAdapter
49
+ participant Bill
50
+ participant Cli as Client
51
+ participant WSFE as AFIP WSFE
52
+ Host->>Adapter: new(bill, token, sign, cuit, pkey, cert)
53
+ Host->>Adapter: set_bill_number!
54
+ Adapter->>Cli: call(:fe_comp_ultimo_autorizado)
55
+ Cli->>WSFE: SOAP
56
+ WSFE-->>Adapter: cbte_nro
57
+ Adapter->>Bill: number = cbte_nro + 1
58
+ Host->>Adapter: authorize!
59
+ Adapter->>Bill: valid?
60
+ alt bill invalido
61
+ Adapter-->>Host: false
62
+ else valido
63
+ Adapter->>Adapter: build_body_request (FeCAEReq)
64
+ Adapter->>Cli: call(:fecae_solicitar)
65
+ Cli->>WSFE: SOAP
66
+ WSFE-->>Adapter: FECAEDetResponse
67
+ Adapter->>Bill: cae, due_date_cae, result, number
68
+ Adapter->>Adapter: parse_errors / parse_events / parse_observations
69
+ Adapter-->>Host: true
70
+ end
71
+ ```
72
+
73
+ ## 3. Inferencias
74
+
75
+ | ítem | confidence | a verificar |
76
+ |---|---|---|
77
+ | `set_bill_number!` se llama antes de `authorize!` | inferred | el README los muestra en ese orden; confirmar si `authorize!` exige número previo |
78
+ | El parseo en capas no propaga (acumula en `errors`/`afip_*`) | declared | diseño "No Explota" del README; confirmado en código |
79
+
80
+ ## 4. Cobertura y fronteras
81
+
82
+ - `invoice_informed?` (lectura `fe_comp_consultar`) no diagramado — acreta en próximo PR que lo toque.
83
+ - La firma CMS/OpenSSL detallada (PKCS7) no se desglosa; ver `build_cms`.
84
+ - Comportamiento de fallo (timeout/degradación) → `docs/consumed/afip.md §e`.
@@ -0,0 +1,94 @@
1
+ # Configuración runtime — snoopy_afip
2
+ > meta: artefacto · RFC-012 · generado arch-structure · enriquecido arch-enrich · anclado a 7813cf2 · cobertura: inventario base 14/14; §f 6/14
3
+
4
+ ## 2.a Hecho verificable
5
+
6
+ | métrica | valor |
7
+ |---|---|
8
+ | total opciones | 14 |
9
+ | requeridas | 0 (ninguna lanza sin default; faltantes fallan en uso, no en boot) |
10
+ | con default | 3 (`open_timeout`, `read_timeout`, `SNOOPY_SSL_VERSION`) |
11
+ | derivadas | 0 |
12
+ | secretas | 2 (`pkey`, `cert`) |
13
+ | origen ENV | 1 (`SNOOPY_SSL_VERSION`) |
14
+
15
+ Es **gema con configuración pública**: el grueso se setea por código en el host (`Snoopy.<attr> = …`, típicamente un initializer), no por ENV.
16
+
17
+ ## 2.b Inventario base
18
+
19
+ | nombre | tipo | requerida | default | origen | consumidor | secret? |
20
+ |---|---|---|---|---|---|---|
21
+ | `Snoopy.cuit` | String | no | `nil` | code-default | `auth_hash`, `AuthorizeAdapter#auth` | no |
22
+ | `Snoopy.sale_point` | String | no | `nil` | code-default | host / `Bill#sale_point` | no |
23
+ | `Snoopy.service_url` | String (URL/WSDL) | no | `nil` | code-default | `AuthorizeAdapter#client_configuration:186` | no |
24
+ | `Snoopy.auth_url` | String (URL/WSDL) | no | `nil` | code-default | `AuthenticationAdapter#client_configuration:115` | no |
25
+ | `Snoopy.pkey` | String (path/PEM) | no | `nil` | code-default | `AuthorizeAdapter#client_configuration:193` | **sí** |
26
+ | `Snoopy.cert` | String (path/PEM) | no | `nil` | code-default | `AuthorizeAdapter#client_configuration:192` | **sí** |
27
+ | `Snoopy.default_document_type` | String | no | `nil` | code-default | `Bill#initialize:30` | no |
28
+ | `Snoopy.default_concept` | String | no | `nil` | code-default | `Bill#initialize:22` | no |
29
+ | `Snoopy.default_currency` | Symbol | no | `nil` | code-default | `Bill#initialize:24` | no |
30
+ | `Snoopy.own_iva_cond` | Symbol | no | `nil` | code-default | host | no |
31
+ | `Snoopy.verbose` | String/bool | no | `nil` | code-default | `snoopy_afip.rb:55` (comentado) | no |
32
+ | `Snoopy.open_timeout` | Integer | no | `30` | code-default (`lib/snoopy_afip.rb:23`) | `Client#call:10`, `AuthorizeAdapter:191` | no |
33
+ | `Snoopy.read_timeout` | Integer | no | `30` | code-default (`lib/snoopy_afip.rb:24`) | `AuthorizeAdapter:190` | no |
34
+ | `SNOOPY_SSL_VERSION` | Symbol | no | `:TLSv1` | env (`constants.rb:24`) | `client_configuration` (ambos adapters) | no |
35
+
36
+ ## 2.c Meta-templates
37
+
38
+ Ninguna (sin patrones `{SERVICE}_{HOST|PORT}` repetidos).
39
+
40
+ ## 2.d Derivaciones simples
41
+
42
+ - `Snoopy::SNOOPY_SSL_VERSION = (ENV['SNOOPY_SSL_VERSION'] || 'TLSv1').to_sym` — leído **una vez al cargar** `constants.rb` y **congelado** como constante (cambiar el ENV en runtime no tiene efecto).
43
+
44
+ ## 2.e Scheduling
45
+
46
+ `n/a` (sin sidekiq/queue/cron).
47
+
48
+ ## 2.i Inyecciones al host
49
+
50
+ - **Monkey-patches globales** (cargados incondicionalmente en `lib/snoopy_afip.rb:7-9`): `core_ext/float.rb`, `core_ext/hash.rb`, `core_ext/string.rb` reabren `Float`/`Hash`/`String`. `Float#round_with_precision`/`round_up_with_precision` y `String#underscore` se redefinen **siempre**; los de `Hash` solo `unless method_defined?`. Sin Railtie/Engine.
51
+
52
+ ## 2.j Inyección a gemas configuradas
53
+
54
+ `—` (no aplica: no configura gemas de terceros vía bloque).
55
+
56
+ ## 3. Inferencias
57
+
58
+ | ítem | confidence | a verificar |
59
+ |---|---|---|
60
+ | `secret?` de `pkey`/`cert` por contenido (clave privada + certificado), no por nombre | inferred | confirmar manejo (¿path en disco vs PEM inline?) |
61
+ | `verbose` consumido solo en línea comentada (`snoopy_afip.rb:55`) | declared | accessor vivo pero sin efecto actual — posible config muerta |
62
+
63
+ ## f. Enriquecimiento semántico
64
+
65
+ > cobertura: 6/14 vars enriquecidas; ausencia ≠ "no aplica".
66
+
67
+ ### f.1 credenciales (`cuit`, `pkey`, `cert`)
68
+
69
+ | var | categoría | failure-mode | side-effect | scope-override | business-reason / definición |
70
+ |---|---|---|---|---|---|
71
+ | `Snoopy.pkey` | infra | runtime-error al firmar CMS (`CmsBuilder`) si falta/inválida | restart (se relee por request al construir cliente) | mutable-singleton | clave privada que firma el CMS de WSAA y el mTLS de WSFE; **secreto** |
72
+ | `Snoopy.cert` | infra | runtime-error (`CmsBuilder`) / handshake TLS si inválido | restart | mutable-singleton | certificado emitido por AFIP; identifica al emisor; **secreto** |
73
+ | `Snoopy.cuit` | business | request rechazado por AFIP si no coincide con el cert | per-request | mutable-singleton | CUIT del emisor; debe coincidir con el del trámite del certificado |
74
+
75
+ ### f.2 endpoints (`auth_url`, `service_url`)
76
+
77
+ | var | categoría | failure-mode | side-effect | scope-override | business-reason / definición |
78
+ |---|---|---|---|---|---|
79
+ | `Snoopy.auth_url` | integration | `ClientError` (WSDL inalcanzable) | restart | mutable-singleton | WSDL de WSAA; **homologación vs producción** se elige acá (riesgo: emitir contra prod por error) |
80
+ | `Snoopy.service_url` | integration | `ClientError` | restart | mutable-singleton | WSDL de WSFE; mismo riesgo homo/prod |
81
+
82
+ ### f.3 tuning de red (`open_timeout`, `read_timeout`, `SNOOPY_SSL_VERSION`)
83
+
84
+ | var | categoría | failure-mode | side-effect | scope-override | business-reason / definición |
85
+ |---|---|---|---|---|---|
86
+ | `SNOOPY_SSL_VERSION` | tuning | handshake TLS falla si AFIP no soporta la versión | **compile-time-only** (congelado al cargar `constants.rb`) | boot-only | default `:TLSv1` quedó **desactualizado** — AFIP exige TLS ≥1.2; revisar (`inferred`) |
87
+
88
+ **Ramificadores:** `issuer_iva_cond = :responsable_monotributo` ramifica `alicivas` (monotributo no informa IVA — `authorize_adapter.rb:75`). Es ramificador de payload, no intra-config.
89
+
90
+ ## 4. Cobertura y fronteras
91
+
92
+ - **Valores reales prohibidos**: solo shape. CUIT/pkey/cert se setean en el host; nunca commitear valores.
93
+ - `ENV["CUIT"]` aparece en `spec/spec_helper.rb` pero es **config de test**, no de la gema → ver `docs/test/`.
94
+ - Enriquecimiento (`categoría`, `failure-mode`, `side-effect`, `business reason`, threading) → arch-enrich (§f).
@@ -0,0 +1,61 @@
1
+ # Dependencias consumidas — AFIP (WSAA + WSFE)
2
+ > meta: artefacto · RFC-018 · generado arch-structure · enriquecido arch-enrich · anclado a 7813cf2 · cobertura: subset SOAP invocado; §c 1/1 · §e 1/1
3
+
4
+ Entrada de **familia** (RFC-018 §4 r2): ambos servicios externos se invocan por el mismo cliente base `Snoopy::Client` (wrapper de `Savon.client`). Difieren en WSDL, headers y operaciones.
5
+
6
+ ## 2.a Identidad
7
+
8
+ | campo | valor |
9
+ |---|---|
10
+ | proveedor / servicio | AFIP (Argentina) — Facturación Electrónica |
11
+ | sub-tipo | **externo** |
12
+ | transporte | SOAP sobre HTTP/HTTPS |
13
+ | cliente nuestro | `Snoopy::Client` (`lib/snoopy_afip/client.rb`) sobre `savon` |
14
+ | auth | WSAA: firma CMS/PKCS7 (cert + pkey) → `token`/`sign`. WSFE: `Auth = {Token, Sign, Cuit}` + mTLS (`ssl_cert_file`/`ssl_cert_key_file`) |
15
+ | ancla | doc oficial AFIP — ver §5 |
16
+
17
+ ### Miembros de la familia
18
+
19
+ | miembro | WSDL (config) | cliente | particularidades |
20
+ |---|---|---|---|
21
+ | **WSAA** (autenticación) | `Snoopy.auth_url` | `AuthenticationAdapter#client_configuration` | `ssl_version`, `pretty_print_xml`; sin headers extra |
22
+ | **WSFE** (facturación electrónica v1) | `Snoopy.service_url` | `AuthorizeAdapter#client_configuration` | headers `Accept-Encoding`/`Connection`; namespace `http://ar.gov.afip.dif.FEV1/`; mTLS con `cert`/`pkey`; `read_timeout`/`open_timeout` |
23
+
24
+ ## 2.b Operaciones consumidas (subset usado)
25
+
26
+ | servicio | operación SOAP | destino (código) | qué mandamos / esperamos |
27
+ |---|---|---|---|
28
+ | WSAA | `:login_cms` | `AuthenticationAdapter#authenticate!` | enviamos CMS firmado (TRA `loginTicketRequest`, service=`wsfe`); esperamos `loginTicketResponse` con `credentials.token`/`.sign` + `header.expirationTime` |
29
+ | WSFE | `:fecae_solicitar` | `AuthorizeAdapter#authorize!` | enviamos `Auth` + `FeCAEReq` (cabecera + detalle: tipo, punto venta, importes, IVA, doc, fechas); esperamos `FECAEDetResponse` con `cae`, `cae_fch_vto`, `resultado`, `cbte_desde` + `errors`/`events`/`observaciones` |
30
+ | WSFE | `:fe_comp_ultimo_autorizado` | `AuthorizeAdapter#set_bill_number!` | enviamos `Auth` + `PtoVta` + `CbteTipo`; esperamos `cbte_nro` (último autorizado) → `+1` |
31
+ | WSFE | `:fe_comp_consultar` | `AuthorizeAdapter#invoice_informed?` | enviamos `Auth` + `FeCompConsReq` (tipo, nro, punto venta); esperamos `result_get` con estado del comprobante |
32
+
33
+ ## 2.c Semántica de retry / idempotencia
34
+
35
+ - **La gema NO reintenta**: `Client#call` envuelve la llamada en `Timeout` y al fallar levanta `ServerTimeout`/`ClientError` — el retry queda a cargo del host.
36
+ - **`authorize!` (`fecae_solicitar`) NO es idempotente por sí solo**: reenviar el mismo `FeCAEReq` puede generar un comprobante nuevo. La idempotencia se logra usando `set_bill_number!` (lee el último autorizado) o `invoice_informed?` (`fe_comp_consultar`) para verificar antes de reintentar. Reintentar a ciegas tras timeout es **inseguro**: el comprobante pudo haberse autorizado del lado de AFIP. `inferred` — confirmar política con AFIP.
37
+ - **`set_bill_number!` / `invoice_informed?` (lecturas) son idempotentes**: reintento seguro.
38
+
39
+ ## 2.d Errores del proveedor → mapeo nuestro
40
+
41
+ | condición del proveedor | excepción nuestra (ver `docs/errors/`) |
42
+ |---|---|
43
+ | `Timeout::Error` (corte por `Timeout::timeout(Snoopy.open_timeout)`) | `Snoopy::Exception::ServerTimeout` |
44
+ | cualquier otro fallo de transporte/SOAP (`rescue => e`) | `Snoopy::Exception::ClientError` (con `e.message`) |
45
+ | errores de negocio AFIP (`FeCAEReq` rechazado, etc.) | **no se levantan**: se parsean a `afip_errors`/`afip_events`/`afip_observations` (code→msg) y el flujo sigue |
46
+ | fallo de parseo de la respuesta AFIP | excepciones `Snoopy::Exception::AuthorizeAdapter::*Parser` (ver `docs/errors/`) |
47
+
48
+ Códigos de error documentados por AFIP para WSAA (`coe.notAuthorized`, `cms.*`, `xml.*`, `wsn.*`, `wsaa.*`) están listados como comentario en `authentication_adapter.rb:17-35` — referencia, no se mapean uno a uno.
49
+
50
+ ## 2.e Degradación
51
+
52
+ - Si AFIP **no responde** (timeout/red): `Snoopy::Exception::ServerTimeout`/`ClientError` propaga al host. No hay fallback ni cola en la gema — la emisión del comprobante **no procede**. El host decide reintento/cola.
53
+ - Si AFIP responde pero **rechaza** (errores de negocio): no hay excepción; `afip_errors` se puebla y `bill.result` queda `R`/`P`. La degradación es "comprobante no aprobado", consultable sin reintentar la red.
54
+ - **SLA de AFIP**: no garantizado; ventanas de mantenimiento frecuentes (códigos `wsn.unavailable`/`wsaa.unavailable`). Diseñar el host para tolerar caídas. `inferred`.
55
+
56
+ ## 5. Cobertura y fronteras
57
+
58
+ - Subset invocado, **no** la API completa de WSFE (la gema usa 4 operaciones de las ~decenas del WS).
59
+ - Timeout/SSL: `open_timeout`/`read_timeout` (default 30) y `SNOOPY_SSL_VERSION` parametrizados en el repo → ver `docs/config/`.
60
+ - Doc oficial: [Espec. Técnica WSAA 1.2.0](http://www.afip.gov.ar/ws/WSAA/Especificacion_Tecnica_WSAA_1.2.0.pdf) · [Manual desarrollador WSFE](http://www.afip.gob.ar/fe/documentos/manual_desarrollador_COMPG_v2_9.pdf).
61
+ - WSDLs: WSAA homo `wsaahomo.afip.gov.ar` / prod `wsaa.afip.gov.ar`; WSFE homo `wswhomo.afip.gov.ar` / prod `servicios1.afip.gov.ar`.
@@ -0,0 +1,69 @@
1
+ # Errores / fallos públicos — snoopy_afip
2
+ > meta: artefacto · RFC-020 · generado arch-structure · enriquecido arch-enrich · anclado a 7813cf2 · cobertura: jerarquía `Snoopy::Exception::*`; §c 13/13
3
+
4
+ ## 1. Resumen
5
+
6
+ Gema sin superficie HTTP → no hay códigos de estado (§b n/a). El contrato del unhappy path es la jerarquía de excepciones `Snoopy::Exception::*` que la gema **levanta** hacia el host, más el patrón "no explota": los errores de negocio de AFIP no se levantan, se acumulan en hashes (`afip_errors`/`afip_events`/`afip_observations`) y los fallos de parseo se acumulan en `errors`.
7
+
8
+ ## 2.a Inventario de excepciones públicas
9
+
10
+ | excepción | jerarquía base | qué la levanta |
11
+ |---|---|---|
12
+ | `Snoopy::Exception::Exception` | `< ::StandardError` | base de la gema; `initialize(msg, backtrace)` |
13
+ | `Snoopy::Exception::ClientError` | `< Exception` | `Client#call` ante cualquier fallo no-timeout de la llamada SOAP |
14
+ | `Snoopy::Exception::ServerTimeout` | `< Timeout::Error` | `Client#call` cuando expira `Timeout::timeout(Snoopy.open_timeout)` — **no** hereda de la base de la gema |
15
+ | `Snoopy::Exception::AuthenticationAdapter::CmsBuilder` | `< Exception` | `AuthenticationAdapter#build_cms` si falla la firma PKCS7 / lectura de pkey-cert |
16
+ | `Snoopy::Exception::AuthorizeAdapter::SetBillNumberParser` | `< Exception` | `#set_bill_number!` ante fallo de parseo de `fe_comp_ultimo_autorizado` |
17
+ | `Snoopy::Exception::AuthorizeAdapter::BuildBodyRequest` | `< Exception` | `#build_body_request` si falla armar el `FeCAEReq` |
18
+ | `Snoopy::Exception::AuthorizeAdapter::FecaeSolicitarResultParser` | `< Exception` | `#parse_fecae_solicitar_response` (capa resultado/CAE) |
19
+ | `Snoopy::Exception::AuthorizeAdapter::FecaeResponseParser` | `< Exception` | `#parse_fecae_solicitar_response` (capa response) — se **acumula** en `errors`, no se raise-a |
20
+ | `Snoopy::Exception::AuthorizeAdapter::ObservationParser` | `< Exception` | `#parse_observations` — se acumula en `errors` |
21
+ | `Snoopy::Exception::AuthorizeAdapter::ErrorParser` | `< Exception` | `#parse_errors` — se acumula en `errors` |
22
+ | `Snoopy::Exception::AuthorizeAdapter::EventsParser` | `< Exception` | `#parse_events` — se acumula en `errors` |
23
+ | `Snoopy::Exception::Bill::MissingAttributes` | `< Exception` | validación de presencia (`Bill#valid?`) — mensaje, se acumula en `bill.errors` |
24
+ | `Snoopy::Exception::Bill::InvalidValueAttribute` | `< Exception` | validación de valor estándar (currency/iva/doc/concept) — se acumula en `bill.errors` |
25
+
26
+ ## 2.b Códigos HTTP por superficie
27
+
28
+ `n/a` — la gema no expone superficie HTTP (no hay controllers/routes). El transporte HTTP lo gestiona `savon` internamente.
29
+
30
+ ## 2.c Política por error
31
+
32
+ | excepción | retriable? | backoff | idempotencia req. | acción |
33
+ |---|---|---|---|---|
34
+ | `ServerTimeout` | condicional | sí (host) | **sí** — verificar con `invoice_informed?` antes de reintentar `authorize!` | propagate + report |
35
+ | `ClientError` | condicional | sí (host) | sí (idem) | propagate + report |
36
+ | `AuthenticationAdapter::CmsBuilder` | no | — | — | propagate (config/cert inválido — no reintentable) |
37
+ | `AuthorizeAdapter::BuildBodyRequest` | no | — | — | propagate (datos del bill mal formados) |
38
+ | `AuthorizeAdapter::SetBillNumberParser` | no | — | — | propagate + report (AFIP cambió formato) |
39
+ | `AuthorizeAdapter::FecaeSolicitarResultParser` | no | — | — | propagate + report (capa crítica: revisar `response` cruda) |
40
+ | `…FecaeResponseParser` / `…ObservationParser` / `…ErrorParser` / `…EventsParser` | no | — | — | log/acumular en `errors` (no fatal: el CAE ya pudo parsearse) |
41
+ | `Bill::MissingAttributes` / `Bill::InvalidValueAttribute` | no | — | — | log en `bill.errors` (validación local, corregir input) |
42
+
43
+ Notas:
44
+ - `report` = candidato a Sentry/exis_ray (observabilidad = otra capa).
45
+ - Tras `ServerTimeout`/`ClientError` en `authorize!`, **nunca** reintentar a ciegas: el comprobante pudo haberse emitido en AFIP (ver `docs/consumed/afip.md §c`).
46
+ - `inferred`: la criticidad/acción la deduje del diseño "No Explota" + flujo; el humano confirma la política operacional real.
47
+
48
+ ## 2.d Shape del payload de error
49
+
50
+ No hay payload serializado (no es servicio). Las formas que cruzan la frontera:
51
+
52
+ - **Excepción Ruby**: `message` + `backtrace` (la base guarda `backtrace` en `attr_accessor`).
53
+ - **`bill.errors`**: `Hash` `{ attr_symbol => [String mensajes] }` (validación).
54
+ - **`authorize_adapter.errors`**: colección de excepciones de parseo no-fatales.
55
+ - **`afip_errors` / `afip_events` / `afip_observations`**: `Hash` `{ code => msg }` con lo devuelto por AFIP.
56
+
57
+ ## 3. Inferencias
58
+
59
+ | ítem | confidence | a verificar |
60
+ |---|---|---|
61
+ | `ServerTimeout < Timeout::Error` (no `< Snoopy::Exception::Exception`) | declared | un `rescue Snoopy::Exception::Exception` **no** atrapa timeouts — confirmar si es intencional |
62
+ | `authorize_adapter.rb:182` referencia `Snoopy::Exception::FecompConsultResponseParser` pero la clase definida es `…::AuthorizeAdapter::FecompConsultResponseParser` | declared | el `raise` levantaría `NameError` en ese path — posible bug latente (código existente, no tocar sin decisión) |
63
+ | `errors <<` en varios `parse_*` rescue trata `errors` como Array, pero se inicializa `{}` (Hash) | declared | `Hash#<<` no existe → `NoMethodError` en ese rescue — posible bug latente |
64
+
65
+ ## 4. Cobertura y fronteras
66
+
67
+ - Solo errores **públicos** (cruzan la frontera de la gema). Rescates internos no listados.
68
+ - Errores del proveedor AFIP que consumimos → su mapeo en `docs/consumed/afip.md §d`.
69
+ - Política de retry/acción → `docs/errors` §c, lo completa arch-enrich.