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.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +31 -0
- data/.github/workflows/release.yml +33 -0
- data/AGENTS.md +100 -0
- data/CHANGELOG.md +44 -0
- data/CLAUDE.md +15 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +68 -19
- data/README.md +87 -189
- data/docs/behavior/behavior.md +84 -0
- data/docs/config/configuracion.md +94 -0
- data/docs/consumed/afip.md +61 -0
- data/docs/errors/errors.md +69 -0
- data/docs/glossary/glossary.md +74 -0
- data/docs/interface/interface.md +86 -0
- data/docs/test/testing.md +74 -0
- data/docs/topology/topology.md +44 -0
- data/lib/snoopy_afip/authorize_adapter.rb +5 -5
- data/lib/snoopy_afip/exceptions.rb +12 -0
- data/lib/snoopy_afip/version.rb +1 -1
- data/lib/snoopy_afip.rb +0 -4
- data/skill/SKILL.md +72 -0
- data/skills.yml +17 -0
- data/snoopy_afip.gemspec +5 -3
- data/spec/snoopy_afip/authentication_adapter_spec.rb +20 -0
- data/spec/snoopy_afip/authorize_adapter_spec.rb +37 -0
- data/spec/snoopy_afip/bill_spec.rb +66 -83
- data/spec/snoopy_afip/exceptions_spec.rb +27 -0
- data/spec/spec_helper.rb +16 -20
- metadata +56 -7
- data/CHANGELOG +0 -27
- data/spec/snoopy_afip/authorizer_spec.rb +0 -9
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
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
8
|
+
Versión actual: `4.3.0`.
|
|
66
9
|
|
|
67
|
-
|
|
10
|
+
## Instalación
|
|
68
11
|
|
|
69
12
|
```ruby
|
|
70
|
-
|
|
13
|
+
# Gemfile
|
|
14
|
+
gem 'snoopy_afip'
|
|
71
15
|
```
|
|
72
16
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
Mantiene la generación de versiones pasadas de `Snoopy`
|
|
23
|
+
## Setup (desarrollo)
|
|
88
24
|
|
|
89
25
|
```bash
|
|
90
|
-
|
|
26
|
+
bundle install
|
|
27
|
+
bundle exec rspec # ver estado de la suite en docs/test/testing.md
|
|
91
28
|
```
|
|
92
29
|
|
|
93
|
-
|
|
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
|
-
|
|
32
|
+
## Configuración
|
|
112
33
|
|
|
113
|
-
La
|
|
34
|
+
La gema se configura en el host (típicamente `config/initializers/snoopy.rb`):
|
|
114
35
|
|
|
115
36
|
```ruby
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
57
|
+
### 1. Autenticar (WSAA)
|
|
157
58
|
|
|
158
59
|
```ruby
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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
|
-
|
|
67
|
+
### 2. Crear el comprobante (`Bill`)
|
|
172
68
|
|
|
173
69
|
```ruby
|
|
174
|
-
Snoopy::
|
|
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
|
-
|
|
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
|
-
|
|
87
|
+
### 3. Autorizar (WSFE)
|
|
180
88
|
|
|
181
89
|
```ruby
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
106
|
+
Catálogo y política completos en [`docs/errors/errors.md`](docs/errors/errors.md).
|
|
218
107
|
|
|
219
|
-
|
|
108
|
+
## Índice de artefactos (`docs/`)
|
|
220
109
|
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
234
|
-
-
|
|
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
|
-
|
|
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.
|