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.
@@ -0,0 +1,74 @@
1
+ # Glosario — snoopy_afip
2
+ > meta: artefacto · RFC-009 · generado arch-enrich · anclado a 7813cf2 · cobertura: términos de dominio AFIP materializados en `lib/`; parcial, acreta por PR
3
+
4
+ Bounded context: facturación electrónica AFIP (Argentina). Sin capa de datos (`docs/data/` n/a) → binding a símbolo público estable (ISO 11179), no a tabla.
5
+
6
+ ## WSAA
7
+
8
+ Web Service de Autenticación y Autorización de AFIP. Recibe un CMS firmado (cert + clave privada) y devuelve `token` + `sign` válidos 12h (la gema pide `to = from + 12h`). Es el prerrequisito para operar contra WSFE.
9
+
10
+ **Binding:** `Snoopy::AuthenticationAdapter` (`authentication_adapter.rb`); operación `:login_cms`.
11
+
12
+ ## WSFE
13
+
14
+ Web Service de Facturación Electrónica v1 de AFIP. Autoriza comprobantes y devuelve el CAE. Requiere las credenciales de WSAA en cada request (`Auth`).
15
+
16
+ **Binding:** `Snoopy::AuthorizeAdapter` (`authorize_adapter.rb`); operaciones `:fecae_solicitar`, `:fe_comp_ultimo_autorizado`, `:fe_comp_consultar`.
17
+
18
+ ## CAE (Código de Autorización Electrónico)
19
+
20
+ Código que AFIP otorga al aprobar un comprobante; sin CAE la factura no es válida. Viene con fecha de vencimiento (`cae_fch_vto`).
21
+
22
+ **Binding:** `Bill#cae`, `Bill#due_date_cae`; seteados en `AuthorizeAdapter#parse_fecae_solicitar_response`.
23
+
24
+ ## CMS / TRA
25
+
26
+ **TRA** (Ticket de Requerimiento de Acceso): XML `loginTicketRequest` con `uniqueId`, `generationTime`, `expirationTime` y `service=wsfe`. **CMS**: el TRA firmado en PKCS7 (cert + pkey), base64, que se manda a WSAA.
27
+
28
+ **Binding:** `AuthenticationAdapter#build_tra` (TRA), `#build_cms` (CMS).
29
+
30
+ ## cbte_type (tipo de comprobante)
31
+
32
+ Código AFIP del tipo de comprobante (Factura A/B/C, Nota de Crédito A/B/C). En la gema se **deriva** de la condición IVA del receptor (`receiver_iva_cond`), no se pasa directo.
33
+
34
+ **Binding:** `Bill#cbte_type` → `Snoopy::BILL_TYPE[receiver_iva_cond]`; tabla completa en `Snoopy::CBTE_TYPE`.
35
+
36
+ ## alicivas (alícuotas de IVA)
37
+
38
+ Discriminación de IVA por ítem del comprobante: array de hashes `{id, amount, taxeable_base}` donde `id` es el porcentaje de IVA (0.105, 0.21, 0.27…) mapeado al código AFIP. Monotributistas no las informan.
39
+
40
+ **Binding:** `Bill#alicivas`, `Bill::TAX_ATTRIBUTES`; código AFIP en `Snoopy::ALIC_IVA`; armado en `AuthorizeAdapter#build_body_request` (clave `Iva`).
41
+
42
+ ## Condición IVA — emisor vs receptor
43
+
44
+ Tres conceptos distintos conviven:
45
+ - **`issuer_iva_cond`** (emisor): determina si se informan alícuotas (monotributo no). Valores en `Snoopy::IVA_COND`.
46
+ - **`receiver_iva_cond`** (receptor): determina el `cbte_type` (clave de `Snoopy::BILL_TYPE`).
47
+ - **`receiver_iva_condition`** (RG 5616, "verdadera condición IVA del receptor"): id que AFIP exige en el detalle (`CondicionIVAReceptorId`). Valores en `Snoopy::IVA_COND_RECEIVER`.
48
+
49
+ **Binding:** `Bill#issuer_iva_cond`, `#receiver_iva_cond`, `#receiver_iva_condition` / `#receiver_iva_condition_id`.
50
+
51
+ ## Resultado (A / P / R)
52
+
53
+ Veredicto de AFIP sobre el comprobante: `A` aprobado, `P` aprobado parcial, `R` rechazado.
54
+
55
+ **Binding:** `Bill#result`, `#approved?`, `#partial_approved?`, `#rejected?`.
56
+
57
+ ## afip_errors / afip_events / afip_observations
58
+
59
+ Tres canales que AFIP devuelve en la respuesta, parseados en hashes `code => msg` separados. **Errores**: motivos de rechazo. **Eventos**: avisos de AFIP (ej. cambios futuros). **Observaciones**: motivos por los que no se autorizó. No son excepciones Ruby — el flujo no explota (ver `docs/behavior/`).
60
+
61
+ **Binding:** `AuthorizeAdapter#afip_errors/#afip_events/#afip_observations`.
62
+
63
+ ## 3. Inferencias
64
+
65
+ | término | confidence | nota |
66
+ |---|---|---|
67
+ | `receiver_iva_condition` (RG 5616) | inferred | el comentario del código dice "la verdadera condición de iva del receptor"; confirmar nombre de negocio exacto |
68
+ | Candidato canónico (RFC-019) | inferred | `CAE`, `WSFE`, `condición IVA` probablemente aparecen en otros repos de facturación del fleet → candidatos a glosario canónico; arch-enrich no lo crea, lo flaggea |
69
+
70
+ ## 4. Cobertura y fronteras
71
+
72
+ - Términos materializados en `lib/` actual. Acreta por PR.
73
+ - Sin `Binding:` a tabla (gema sin datos); binding a símbolo público.
74
+ - Significado de negocio profundo (reglas AFIP) fuera de alcance: referencia al manual del desarrollador AFIP (ver `docs/consumed/afip.md`).
@@ -0,0 +1,86 @@
1
+ # Interfaz — snoopy_afip
2
+ > meta: artefacto · RFC-004 · generado arch-structure · anclado a 7813cf2 · cobertura: superficie pública de `lib/snoopy_afip/`
3
+
4
+ ## 1. Resumen
5
+
6
+ API Ruby pública de la gema. Módulo raíz `Snoopy` (configuración global + helpers) y cuatro clases de dominio: `AuthenticationAdapter` (WSAA), `AuthorizeAdapter` (WSFE), `Bill` (comprobante), `Client` (wrapper Savon). Constantes de dominio en `Snoopy::*` y jerarquía de errores en `Snoopy::Exception::*`.
7
+
8
+ ## 2. Símbolos públicos
9
+
10
+ | símbolo | tipo | nota |
11
+ |---|---|---|
12
+ | `Snoopy` | module | raíz; `extend self`; config global vía `attr_accessor` |
13
+ | `Snoopy.cuit` `=` | accessor | CUIT del emisor |
14
+ | `Snoopy.sale_point` `=` | accessor | punto de venta |
15
+ | `Snoopy.service_url` `=` | accessor | WSDL de WSFE |
16
+ | `Snoopy.auth_url` `=` | accessor | WSDL de WSAA |
17
+ | `Snoopy.pkey` `=` | accessor | path/contenido de clave privada |
18
+ | `Snoopy.cert` `=` | accessor | path/contenido de certificado |
19
+ | `Snoopy.default_document_type` `=` | accessor | default de `Bill#document_type` |
20
+ | `Snoopy.default_concept` `=` | accessor | default de `Bill#concept` |
21
+ | `Snoopy.default_currency` `=` | accessor | default de `Bill#currency` |
22
+ | `Snoopy.own_iva_cond` `=` | accessor | condición IVA propia |
23
+ | `Snoopy.verbose` `=` | accessor | flag de verbosidad (Savon) |
24
+ | `Snoopy.open_timeout` `=` | accessor | timeout de apertura; default `30` |
25
+ | `Snoopy.read_timeout` `=` | accessor | timeout de lectura; default `30` |
26
+ | `Snoopy.auth_hash` | method | `{"Token"=>…, "Sign"=>…, "Cuit"=>…}` — ver §3 |
27
+ | `Snoopy.bill_types` | method | array `[etiqueta, código]` de tipos habilitados |
28
+ | `Snoopy::Client` | class | wrapper de `Savon.client` |
29
+ | `Snoopy::Client#initialize(attrs)` | method | `attrs` → opciones Savon |
30
+ | `Snoopy::Client#call(service, args={})` | method | invoca SOAP con `Timeout`; devuelve `.body`; traduce fallos a `ServerTimeout`/`ClientError` |
31
+ | `Snoopy::Client#savon` `=` | accessor | cliente Savon subyacente |
32
+ | `Snoopy::AuthenticationAdapter` | class | flujo WSAA |
33
+ | `…#initialize(attrs={})` | method | `attrs[:pkey]`, `attrs[:cert]` |
34
+ | `…#authenticate!` | method | invoca `:login_cms`; devuelve hash con `:token`, `:sign`, `:expiration_time` |
35
+ | `…#build_tra` | method | arma el TRA (XML) para `wsfe` |
36
+ | `…#build_cms` | method | firma el TRA en PKCS7 → CMS base64 |
37
+ | `….generate_pkey(leng=8192)` | class method | genera clave privada RSA PEM |
38
+ | `….generate_certificate_request_with_ruby(pkey, subj_o, subj_cn, subj_cuit)` | class method | CSR vía OpenSSL Ruby |
39
+ | `….generate_certificate_request_with_bash(pkey, subj_o, subj_cn, subj_cuit)` | class method | CSR vía `openssl req` (shell) |
40
+ | `Snoopy::AuthorizeAdapter` | class | flujo WSFE |
41
+ | `…#initialize(attrs)` | method | `:bill, :cuit, :sign, :pkey, :cert, :token` |
42
+ | `…#authorize!` | method | invoca `:fecae_solicitar`; setea CAE/result en `bill`; devuelve bool |
43
+ | `…#set_bill_number!` | method | invoca `:fe_comp_ultimo_autorizado`; setea `bill.number` |
44
+ | `…#invoice_informed?` | method | invoca `:fe_comp_consultar`; devuelve bool |
45
+ | `…#auth` | method | `{"Token"=>…, "Sign"=>…, "Cuit"=>…}` de la instancia |
46
+ | `…#errors` `=` | accessor | errores internos de parseo (hash de excepciones) |
47
+ | `…#afip_errors` `=` | accessor | errores devueltos por AFIP (`code=>msg`) |
48
+ | `…#afip_events` `=` | accessor | eventos devueltos por AFIP |
49
+ | `…#afip_observations` `=` | accessor | observaciones devueltas por AFIP |
50
+ | `…#request` `=` / `#response` `=` | accessor | payload enviado / respuesta cruda |
51
+ | `Snoopy::Bill` | class | modela el comprobante |
52
+ | `…#initialize(attrs={})` | method | ver §4 README para `attrs` |
53
+ | `…#valid?` | method | corre validaciones; puebla `#errors` |
54
+ | `…#errors` `=` | accessor | hash `attr => [mensajes]` |
55
+ | `…#total` | method | neto + IVA, redondeado 2 |
56
+ | `…#iva_sum` | method | suma de `amount` de `alicivas` |
57
+ | `…#cbte_type` | method | código de comprobante según `receiver_iva_cond` |
58
+ | `…#cbte_asoc_type` | method | código del comprobante asociado |
59
+ | `…#receiver_iva_condition_id` | method | id de condición IVA del receptor (RG 5616) |
60
+ | `…#exchange_rate` | method | `1` si `:peso`; resto sin implementar (ver §3) |
61
+ | `…#approved?` / `#rejected?` / `#partial_approved?` | method | estado del resultado AFIP (`A`/`R`/`P`) |
62
+ | `…#to_h` / `#to_hash` | method | volcado de instance vars |
63
+ | `Snoopy::Bill::ATTRIBUTES` | const | lista de `attr_accessor` del comprobante |
64
+ | `Snoopy::Bill::TAX_ATTRIBUTES` | const | `[:id, :amount, :taxeable_base]` |
65
+ | `Snoopy::Bill::ATTRIBUTES_PRECENSE` | const | atributos requeridos en validación |
66
+ | `Snoopy::CBTE_TYPE` `Snoopy::BILL_TYPE` `Snoopy::CONCEPTS` `Snoopy::DOCUMENTS` `Snoopy::CURRENCY` `Snoopy::ALIC_IVA` `Snoopy::IVA_COND` `Snoopy::IVA_COND_RECEIVER` | const | tablas de dominio (ver `docs/data/` n/a → enriquecer en glosario) |
67
+ | `Snoopy::RESPONSABLE_INSCRIPTO` `…_MONOTRIBUTO` `CONSUMIDOR_FINAL` `IVA_SUJETO_EXENTO` `IVA_NO_ALCANZADO` | const | símbolos de condición IVA |
68
+ | `Snoopy::SNOOPY_SSL_VERSION` | const | versión TLS (de `ENV['SNOOPY_SSL_VERSION']`, default `:TLSv1`) — ver `docs/config/` |
69
+ | `Snoopy::Exception::*` | module/class | jerarquía de errores — ver `docs/errors/` |
70
+ | `String#underscore` `Hash#symbolize_keys[!]` `Hash#deep_symbolize_keys` `Hash#underscore_keys[!]` `Float#round_with_precision` `Float#round_up_with_precision` | core_ext | **monkey-patches globales** sobre `String`/`Hash`/`Float` — contaminan el host (ver §4) |
71
+
72
+ ## 3. Inferencias
73
+
74
+ | ítem | confidence | a verificar |
75
+ |---|---|---|
76
+ | `Bill#exchange_rate` solo resuelve `:peso` (`return 1`); el resto del método está comentado → devuelve `nil` para moneda extranjera | declared | confirmar si es intencional o feature incompleta |
77
+ | `Snoopy.auth_hash` usa `Snoopy::TOKEN` / `Snoopy::SIGN` (constantes **no definidas** en el código leído) | declared | `auth_hash` levantaría `NameError`; `AuthorizeAdapter#auth` es la vía viva — confirmar si `auth_hash` es legacy muerto |
78
+ | `core_ext` define métodos solo `unless method_defined?` (Hash/String) pero `Float` y `String#underscore` se redefinen siempre | declared | colisión potencial con ActiveSupport en hosts Rails |
79
+
80
+ ## 4. Cobertura y fronteras
81
+
82
+ - **Significado de negocio** de constantes/atributos → fuera de alcance (va a `docs/glossary/`, arch-enrich).
83
+ - **Errores** detallados → `docs/errors/` (RFC-020).
84
+ - **Config runtime** (`ENV`, accessors) → `docs/config/` (RFC-012).
85
+ - **core_ext globales**: la gema parchea `String`/`Hash`/`Float` del host — superficie pública de facto aunque no sea el API previsto; el impacto (colisión con ActiveSupport) es concern de comportamiento → arch-enrich.
86
+ - `Snoopy::Exception::ServerTimeout < Timeout::Error` (no hereda de `Snoopy::Exception::Exception`) — detalle en `docs/errors/`.
@@ -0,0 +1,74 @@
1
+ # Test — snoopy_afip
2
+ > meta: artefacto · RFC-013 · generado arch-structure · enriquecido arch-enrich · anclado a 7813cf2 (suite reescrita en fix/specs-and-lock) · cobertura: inventario estructural; §e-§h 4/4
3
+
4
+ ## 1. Resumen
5
+
6
+ Suite RSpec en `spec/`, **reescrita contra la API actual y verde** (`21 examples, 0 failures`). Corre bajo **Ruby 2.7.x** (runtime del consumidor `argentina_invoice_service`). En **Ruby 3.x la gema no carga**: el stack de savon 2.12 (httpi 2.x) depende de `kconv` (removido de stdlib en 3.x) y de `Rack::Utils::HeaderHash` (removido en rack 3) → correr en 3.x requiere el upgrade de savon (gated, ver #13/roadmap).
7
+
8
+ ## 2.a Suites, frameworks y niveles
9
+
10
+ | framework | archivo | propósito | nivel |
11
+ |---|---|---|---|
12
+ | RSpec | `spec/snoopy_afip/bill_spec.rb` | `Bill`: cbte_type, iva_sum/total, exchange_rate, receiver_iva_condition_id, estado del resultado, `valid?` | unit |
13
+ | RSpec | `spec/snoopy_afip/authorize_adapter_spec.rb` | `AuthorizeAdapter`: `auth`, y regresión de rescues de parseo (#10/#16) | unit |
14
+ | RSpec | `spec/snoopy_afip/authentication_adapter_spec.rb` | `AuthenticationAdapter`: `build_tra`, credenciales de instancia | unit |
15
+ | RSpec | `spec/snoopy_afip/exceptions_spec.rb` | jerarquía `Snoopy::Exception` + paraguas `Error` (regresión #14) | unit |
16
+ | RSpec | `spec/spec_helper.rb` | bootstrap + config base de homologación | — |
17
+
18
+ ## 2.b Comando de corrida
19
+
20
+ ```bash
21
+ bundle exec rspec # bajo Ruby 2.7.x
22
+ ```
23
+
24
+ No hay CI declarado (sin `.github/workflows/`, `.circleci/`, `bin/ci`, `config/ci.rb`). Corrida solo local. Dev-deps de test: `rspec ~> 3.13`, `activesupport` (gemspec); pin `rack ~> 2.2` (Gemfile, requerido por httpi 2.x).
25
+
26
+ ## 2.c Fixtures / Factories
27
+
28
+ - **Sin fixtures en disco.** Los specs son unit puros: construyen `Bill`/adapters con placeholders inline; no requieren `spec/fixtures/pkey`/`cert.crt` ni llamadas reales a AFIP.
29
+ - Sin FactoryBot, sin VCR. Las URLs de homologación se setean en `spec_helper.rb` pero ninguna prueba abre conexión (Savon es lazy salvo el build del cliente, que no conecta).
30
+ - `Bill#valid?` necesita `blank?`/`present?` → `spec_helper` carga `active_support/core_ext/object/blank`.
31
+
32
+ ## 2.d Configuración de coverage
33
+
34
+ Ninguna (sin `.simplecov` / `SimpleCov.start`). Sin umbral declarado.
35
+
36
+ ## 2.e Gaps de cobertura
37
+
38
+ **Cubierto** (unit, sin red): lógica de `Bill` (cálculos, validaciones, mapeos de dominio), jerarquía de excepciones + paraguas, `build_tra`, y el **comportamiento de los rescues de parseo** (no explotan, registran String).
39
+
40
+ **NO cubierto** (requiere mock de Savon / VCR / homologación real):
41
+ - `authorize!` / `set_bill_number!` / `invoice_informed?` — el camino feliz contra WSFE (no se mockea la respuesta SOAP).
42
+ - `authenticate!` / `build_cms` — firma CMS con cert/pkey reales.
43
+ - `parse_*` con respuestas AFIP **válidas** (solo se testea el path de error).
44
+ - `core_ext` (`round_with_precision`, `deep_symbolize_keys`, `underscore`).
45
+
46
+ ## 2.f Contract-assessment
47
+
48
+ | contrato público | test? |
49
+ |---|---|
50
+ | operaciones (RFC-003) | n/a (gema sin superficie) |
51
+ | dependencias consumidas WSAA/WSFE (RFC-018) | **parcial** — `build_tra` sí; las llamadas SOAP no se ejercitan (sin mock/VCR) |
52
+ | errores públicos (RFC-020) | **sí** — jerarquía `Snoopy::Exception::*` + paraguas `Error` + comportamiento de los rescues |
53
+
54
+ ## 2.g Link a incidente
55
+
56
+ - `exceptions_spec.rb` fija la jerarquía de `ServerTimeout` (regresión de **#14**).
57
+ - `authorize_adapter_spec.rb` fija que los rescues de parseo no explotan y registran String (regresión de **#10/#16** — el diseño "No Explota" que nació de un incidente real con cambios de formato de AFIP).
58
+
59
+ ## 2.h PII en fixtures
60
+
61
+ - Sin fixtures con PII (no hay fixtures en disco). `pkey`/`cert`/`cuit` en specs son placeholders inline (`/tmp/pkey`, `"20111111112"`), nunca material real.
62
+
63
+ ## 3. Inferencias
64
+
65
+ | ítem | confidence | a verificar |
66
+ |---|---|---|
67
+ | La gema no carga en Ruby 3.x (kconv/httpi 2.x, Rack::Utils::HeaderHash/rack 3) | declared | desbloquea con el upgrade de savon a 2.15+ (httpi 4) — gated |
68
+ | El lock está resuelto para Ruby 2.7 (nokogiri 1.15.7, rack 2.2) | declared | un dev en Ruby 3.x no podrá `bundle install` hasta el upgrade |
69
+
70
+ ## 4. Cobertura y fronteras
71
+
72
+ - El contenido detallado de cada test queda en el código.
73
+ - Falta cobertura de los caminos felices contra AFIP (necesita mock de Savon o VCR) — próximo incremento.
74
+ - La corrida en Ruby 3.x está bloqueada por el stack de dependencias, no por los specs.
@@ -0,0 +1,44 @@
1
+ # Topología — snoopy_afip
2
+ > meta: artefacto · RFC-006 · generado arch-structure · anclado a 7813cf2 · cobertura: deps de runtime + servicios externos AFIP
3
+
4
+ ## 1. Resumen
5
+
6
+ Gema cliente SOAP. Una sola dependencia de runtime declarada en el `.gemspec` (`savon`), que arrastra el stack SOAP (httpi, nori, gyoku, akami, wasabi, nokogiri). Consume dos servicios SOAP externos de AFIP: WSAA (auth) y WSFE (facturación). No corre como proceso propio: se embebe en el host (típicamente una app Rails).
7
+
8
+ ## 2. Dependencias
9
+
10
+ | nombre | versión | rol |
11
+ |---|---|---|
12
+ | `savon` | `~> 2.12.1` (gemspec) · `2.12.0` (lock) | cliente SOAP — única dep de runtime declarada |
13
+ | `httpi` | `2.4.4` (lock, transitiva) | capa HTTP de savon |
14
+ | `nori` | `2.6.0` (lock, transitiva) | XML→Hash (usado directo en `AuthenticationAdapter`) |
15
+ | `nokogiri` | `1.10.9` (lock, transitiva) | parser XML; usado directo en `build_tra` (`Nokogiri::XML::Builder`) |
16
+ | `gyoku` `akami` `wasabi` `builder` `rack` `socksify` `mini_portile2` | ver lock | transitivas de savon |
17
+ | `OpenSSL` (stdlib) | — | firma CMS/PKCS7, generación de clave/CSR |
18
+ | `Timeout` (stdlib) | — | corta llamadas SOAP (`Snoopy::Client#call`) |
19
+
20
+ ## 3. Grafo
21
+
22
+ ```mermaid
23
+ flowchart LR
24
+ Host["App host (Rails)"] -->|require| Snoopy["snoopy_afip"]
25
+ Snoopy -->|Savon.client| Savon["savon (SOAP)"]
26
+ Savon -->|HTTP/SOAP| WSAA["AFIP WSAA<br>login_cms"]
27
+ Savon -->|HTTP/SOAP| WSFE["AFIP WSFE<br>fecae_solicitar / fe_comp_*"]
28
+ Snoopy -->|firma CMS| OpenSSL["OpenSSL (stdlib)"]
29
+ Snoopy -->|build_tra| Nokogiri["nokogiri"]
30
+ Snoopy -->|parse| Nori["nori"]
31
+ ```
32
+
33
+ ## 4. Modos de ejecución
34
+
35
+ | modo | aplica | nota |
36
+ |---|---|---|
37
+ | librería embebida | sí | se usa dentro del proceso del host; sin daemon/worker propio |
38
+ | web / worker / cron propios | no | la gema no define procesos |
39
+
40
+ ## 5. Cobertura y fronteras
41
+
42
+ - **`Gemfile.lock` divergente** (ver `docs/test/` y nota global): el lock fija `snoopy_afip (4.0.1)` mientras `version.rb` dice `4.3.0`, y lista en `PATH specs` deps (`akami`, `nokogiri`, `nori`, `wasabi`) que el `.gemspec` **actual** no declara explícitamente (solo `savon`). El lock parece no regenerado tras cambios de gemspec/versión. Versiones de la tabla tomadas del lock como mejor evidencia disponible, marcadas.
43
+ - Servicios AFIP externos (WSAA/WSFE): contrato consumido en `docs/consumed/` (RFC-018).
44
+ - Topología upstream de AFIP (infra del organismo) fuera de alcance.
@@ -114,7 +114,7 @@ module Snoopy
114
114
  [obs].flatten.each { |ob| afip_observations[ob[:code]] = ob[:msg] }
115
115
  end
116
116
  rescue => e
117
- errors << Snoopy::Exception::AuthorizeAdapter::ObservationParser.new(e.message, e.backtrace)
117
+ @errors[:observation_parser] = Snoopy::Exception::AuthorizeAdapter::ObservationParser.new(e.message, e.backtrace).message
118
118
  end
119
119
 
120
120
  def parse_events(fecae_events)
@@ -122,7 +122,7 @@ module Snoopy
122
122
  [events].flatten.each { |event| afip_events[event[:code]] = event[:msg] }
123
123
  end
124
124
  rescue => e
125
- errors << Snoopy::Exception::AuthorizeAdapter::EventsParser.new(e.message, e.backtrace)
125
+ @errors[:events_parser] = Snoopy::Exception::AuthorizeAdapter::EventsParser.new(e.message, e.backtrace).message
126
126
  end
127
127
 
128
128
  def parse_errors(fecae_errors)
@@ -130,7 +130,7 @@ module Snoopy
130
130
  [errores].flatten.each { |error| afip_errors[error[:code]] = error[:msg] }
131
131
  end
132
132
  rescue => e
133
- errors << Snoopy::Exception::AuthorizeAdapter::ErrorParser.new(e.message, e.backtrace)
133
+ @errors[:error_parser] = Snoopy::Exception::AuthorizeAdapter::ErrorParser.new(e.message, e.backtrace).message
134
134
  end
135
135
 
136
136
  def parse_fecae_solicitar_response
@@ -154,7 +154,7 @@ module Snoopy
154
154
  parse_errors(fecae_result[:errors]) if fecae_result.has_key? :errors
155
155
  parse_events(fecae_result[:events]) if fecae_result.has_key? :events
156
156
  rescue => e
157
- @errors << Snoopy::Exception::AuthorizeAdapter::FecaeResponseParser.new(e.message, e.backtrace)
157
+ @errors[:fecae_response_parser] = Snoopy::Exception::AuthorizeAdapter::FecaeResponseParser.new(e.message, e.backtrace).message
158
158
  end
159
159
  end
160
160
 
@@ -179,7 +179,7 @@ module Snoopy
179
179
  self.parse_errors(fe_comp_consultar_result[:errors]) if fe_comp_consultar_result and fe_comp_consultar_result.has_key? :errors
180
180
  self.parse_events(fe_comp_consultar_result[:events]) if fe_comp_consultar_result and fe_comp_consultar_result.has_key? :events
181
181
  rescue => e
182
- @errors << Snoopy::Exception::FecompConsultResponseParser.new(e.message, e.backtrace)
182
+ @errors[:fecomp_consult_response_parser] = Snoopy::Exception::AuthorizeAdapter::FecompConsultResponseParser.new(e.message, e.backtrace).message
183
183
  end
184
184
 
185
185
  def client_configuration
@@ -1,7 +1,18 @@
1
+ require "timeout" # ServerTimeout < Timeout::Error; en Ruby 3.4+ timeout es default gem no autocargado
2
+
1
3
  module Snoopy
2
4
  module Exception
3
5
 
6
+ # Paraguas de todos los errores que emite la gema. Se incluye en la base
7
+ # `Exception` (y por herencia en todas sus subclases) y en `ServerTimeout`,
8
+ # que debe seguir siendo un `Timeout::Error` por compatibilidad hacia atrás.
9
+ # Permite `rescue Snoopy::Exception::Error` para atrapar cualquier error de
10
+ # la gema, incluido el timeout, sin romper `rescue Timeout::Error`.
11
+ module Error; end
12
+
4
13
  class Exception < ::StandardError
14
+ include Error
15
+
5
16
  attr_accessor :backtrace
6
17
 
7
18
  def initialize(msg, backtrace)
@@ -17,6 +28,7 @@ module Snoopy
17
28
  end
18
29
 
19
30
  class ServerTimeout < Timeout::Error
31
+ include Error
20
32
  end
21
33
 
22
34
  module AuthenticationAdapter
@@ -1,3 +1,3 @@
1
1
  module Snoopy
2
- VERSION = "4.3.0"
2
+ VERSION = "4.3.1"
3
3
  end
data/lib/snoopy_afip.rb CHANGED
@@ -23,10 +23,6 @@ module Snoopy
23
23
  self.open_timeout ||= 30
24
24
  self.read_timeout ||= 30
25
25
 
26
- def auth_hash
27
- {"Token" => Snoopy::TOKEN, "Sign" => Snoopy::SIGN, "Cuit" => Snoopy.cuit}
28
- end
29
-
30
26
  def bill_types
31
27
  [
32
28
  ["Factura A", "01"],
data/skill/SKILL.md ADDED
@@ -0,0 +1,72 @@
1
+ ---
2
+ name: snoopy-afip
3
+ description: >
4
+ Gema Ruby `snoopy_afip` — adaptador de Facturación Electrónica de AFIP
5
+ (Argentina). Resuelve los servicios SOAP WSAA (autenticación: token/sign a
6
+ partir de cert+pkey) y WSFE (autorización de comprobantes: devuelve CAE) vía
7
+ `savon`. Activá esta skill cuando trabajes con emisión de facturas/notas de
8
+ crédito electrónicas argentinas, autenticación contra AFIP, el módulo
9
+ `Snoopy`, o las clases `AuthenticationAdapter` / `AuthorizeAdapter` / `Bill`.
10
+ triggers:
11
+ - "facturación electrónica AFIP"
12
+ - "emitir factura electrónica argentina"
13
+ - "autenticar contra WSAA"
14
+ - "autorizar comprobante WSFE"
15
+ - "obtener CAE de AFIP"
16
+ - "usar la gema snoopy_afip"
17
+ - "Snoopy::AuthorizeAdapter / Snoopy::Bill"
18
+ ---
19
+
20
+ # snoopy_afip — skill de agente
21
+
22
+ Contrato resumido de la gema. El detalle vive en `docs/<capa>/` (anclado a `4.3.0` / commit `7813cf2`); esta skill indexa y resume.
23
+
24
+ ## Qué es / cuándo usar
25
+
26
+ Adaptador de la Facturación Electrónica de AFIP. Úsala para autenticar (WSAA) y autorizar comprobantes (WSFE) desde Ruby. Se configura en el host vía `Snoopy.<attr> = …` y se opera con tres clases. No corre como proceso propio: es librería embebida.
27
+
28
+ ## Contrato resumido (piso mínimo)
29
+
30
+ **Flujo típico** (autenticar → numerar → autorizar):
31
+
32
+ ```ruby
33
+ auth = Snoopy::AuthenticationAdapter.new(pkey: pkey, cert: cert)
34
+ creds = auth.authenticate! # {token, sign, expiration_time} (12h)
35
+
36
+ bill = Snoopy::Bill.new(cuit:, sale_point:, concept:, document_type:, document_num:,
37
+ issuer_iva_cond:, receiver_iva_cond:, receiver_iva_condition:,
38
+ total_net:, alicivas:) # alicivas: [{id:, amount:, taxeable_base:}]
39
+ adapter = Snoopy::AuthorizeAdapter.new(bill:, pkey:, cert:, cuit:,
40
+ token: creds[:token], sign: creds[:sign])
41
+ adapter.set_bill_number! # último autorizado + 1
42
+ adapter.authorize! # setea bill.cae / bill.result
43
+ ```
44
+
45
+ **Símbolos clave:** `Snoopy` (config global), `Snoopy::AuthenticationAdapter` (WSAA), `Snoopy::AuthorizeAdapter` (WSFE), `Snoopy::Bill` (comprobante), `Snoopy::Client` (wrapper Savon). Tablas de dominio: `Snoopy::BILL_TYPE`, `ALIC_IVA`, `IVA_COND_RECEIVER`, `DOCUMENTS`, `CURRENCY`.
46
+
47
+ **Gotchas (load-bearing):**
48
+ - `receiver_iva_cond` (key de `BILL_TYPE`) determina el `cbte_type` — distinto de `issuer_iva_cond` y de `receiver_iva_condition` (RG 5616). Ver glosario.
49
+ - **Tras timeout en `authorize!` no reintentar a ciegas**: el comprobante pudo emitirse en AFIP — verificar con `invoice_informed?` (`docs/consumed/afip.md §c`).
50
+ - AFIP exige TLS ≥1.2; el default `SNOOPY_SSL_VERSION=:TLSv1` quedó desactualizado (`docs/config/configuracion.md §f`).
51
+ - `pkey`/`cert`/`cuit` son secretos: setear en el host, nunca commitear.
52
+ - La gema **parchea globalmente** `String`/`Hash`/`Float` (core_ext) — posible colisión con ActiveSupport.
53
+
54
+ ## Índice de artefactos
55
+
56
+ | capa | path | nota |
57
+ |---|---|---|
58
+ | interfaz | [`docs/interface/interface.md`](../docs/interface/interface.md) | API Ruby pública |
59
+ | comportamiento | [`docs/behavior/behavior.md`](../docs/behavior/behavior.md) | secuencias WSAA/WSFE |
60
+ | glosario | [`docs/glossary/glossary.md`](../docs/glossary/glossary.md) | términos AFIP |
61
+ | consumidas | [`docs/consumed/afip.md`](../docs/consumed/afip.md) | SOAP WSAA + WSFE |
62
+ | errores | [`docs/errors/errors.md`](../docs/errors/errors.md) | excepciones + política |
63
+ | configuración | [`docs/config/configuracion.md`](../docs/config/configuracion.md) | opciones runtime |
64
+ | topología | [`docs/topology/topology.md`](../docs/topology/topology.md) | deps + externos |
65
+ | test | [`docs/test/testing.md`](../docs/test/testing.md) | suite + gaps |
66
+ | operaciones (api) · datos · eventos · multi-tenancy | n/a | gema SOAP sin HTTP/DB/eventos |
67
+
68
+ ## Uso correcto / gotchas
69
+
70
+ - Homologación vs producción se elige por `auth_url`/`service_url` — riesgo de emitir contra prod por error.
71
+ - Errores de negocio de AFIP **no** levantan excepción: se leen en `afip_errors`/`afip_events`/`afip_observations`.
72
+ - La suite RSpec actual está desactualizada respecto al código (ver `docs/test/testing.md`) — no asumir cobertura.
data/skills.yml ADDED
@@ -0,0 +1,17 @@
1
+ mcps:
2
+ - github
3
+ - clickup
4
+ skills:
5
+ multi-vendor-feedback:
6
+ yard:
7
+ quality-code:
8
+ gem-release:
9
+ arch-structure:
10
+ arch-compose:
11
+ arch-enrich:
12
+ skill-feedback:
13
+ agent-issue:
14
+ bug-report:
15
+ dev-flow:
16
+ documentation-writer:
17
+ matrix-element:
data/snoopy_afip.gemspec CHANGED
@@ -7,9 +7,8 @@ Gem::Specification.new do |s|
7
7
  s.name = "snoopy_afip"
8
8
  s.version = Snoopy::VERSION
9
9
 
10
- s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
10
+ s.required_ruby_version = ">= 2.5"
11
11
  s.authors = ["g.edera"]
12
- s.date = "2017-06-29"
13
12
  s.description = "Adaptador para Web Service de Facturación Electrónica Argentina (AFIP)"
14
13
  s.email = "gab.edera@gmail.com"
15
14
  s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
@@ -18,7 +17,10 @@ Gem::Specification.new do |s|
18
17
  s.require_paths = ["lib"]
19
18
  # s.rubygems_version = "1.8.25"
20
19
  s.summary = "Adaptador AFIP wsfe."
21
- s.test_files = ["spec/snoopy_afip/authorizer_spec.rb", "spec/snoopy_afip/bill_spec.rb", "spec/spec_helper.rb"]
20
+ s.test_files = Dir["spec/**/*"]
22
21
 
23
22
  s.add_dependency 'savon', '~> 2.12.1'
23
+
24
+ s.add_development_dependency 'rspec', '~> 3.13'
25
+ s.add_development_dependency 'activesupport' # Bill#valid? usa blank?/present?
24
26
  end
@@ -0,0 +1,20 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/../spec_helper")
2
+
3
+ RSpec.describe Snoopy::AuthenticationAdapter do
4
+ describe "#build_tra" do
5
+ it "construye el TRA (loginTicketRequest) para el servicio wsfe" do
6
+ xml = described_class.new.build_tra
7
+ expect(xml).to include("loginTicketRequest")
8
+ expect(xml).to include("wsfe")
9
+ expect(xml).to include("uniqueId")
10
+ end
11
+ end
12
+
13
+ describe "credenciales de instancia" do
14
+ it "expone pkey y cert recibidos" do
15
+ auth = described_class.new(pkey: "/tmp/pkey", cert: "/tmp/cert")
16
+ expect(auth.pkey).to eq("/tmp/pkey")
17
+ expect(auth.cert).to eq("/tmp/cert")
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,37 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/../spec_helper")
2
+
3
+ RSpec.describe Snoopy::AuthorizeAdapter do
4
+ subject(:adapter) do
5
+ described_class.new(bill: nil, cuit: "20111111112", sign: "SIGN", token: "TOKEN",
6
+ pkey: "/tmp/pkey", cert: "/tmp/cert")
7
+ end
8
+
9
+ describe "#auth" do
10
+ it "arma el hash de credenciales para AFIP" do
11
+ expect(adapter.auth).to eq("Token" => "TOKEN", "Sign" => "SIGN", "Cuit" => "20111111112")
12
+ end
13
+ end
14
+
15
+ # Regresión #10/#16: los rescues de parseo registraban con `errors << ...`
16
+ # sobre un Hash (NoMethodError) y la línea 182 usaba una constante mal
17
+ # namespaceada (NameError). Ahora registran en @errors un String (.message)
18
+ # y NO propagan. Se dispara pasando datos malformados (nil) a cada parser.
19
+ describe "rescues de parseo (no explotan)" do
20
+ {
21
+ parse_errors: :error_parser,
22
+ parse_events: :events_parser,
23
+ parse_observations: :observation_parser
24
+ }.each do |method, key|
25
+ it "##{method} con dato inválido registra un String en errors[#{key.inspect}] sin levantar" do
26
+ expect { adapter.public_send(method, nil) }.not_to raise_error
27
+ expect(adapter.errors[key]).to be_a(String)
28
+ end
29
+ end
30
+
31
+ it "parse_fe_comp_consultar_response con response vacío no levanta NameError" do
32
+ adapter.response = {}
33
+ expect { adapter.parse_fe_comp_consultar_response }.not_to raise_error
34
+ expect(adapter.errors[:fecomp_consult_response_parser]).to be_a(String)
35
+ end
36
+ end
37
+ end