snoopy_afip 4.2.2 → 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 +14 -13
- data/lib/snoopy_afip/bill.rb +16 -3
- data/lib/snoopy_afip/constants.rb +13 -0
- 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
|
@@ -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.
|
|
@@ -62,14 +62,15 @@ module Snoopy
|
|
|
62
62
|
# today = Time.new.in_time_zone('Buenos Aires').strftime('%Y%m%d')
|
|
63
63
|
today = Date.today.strftime('%Y%m%d')
|
|
64
64
|
fecaereq = {"FeCAEReq" => { "FeCabReq" => { "CantReg" => "1", "CbteTipo" => bill.cbte_type, "PtoVta" => bill.sale_point },
|
|
65
|
-
"FeDetReq" => { "FECAEDetRequest" => { "Concepto"
|
|
66
|
-
"DocTipo"
|
|
67
|
-
"CbteFch"
|
|
68
|
-
"ImpTotConc"
|
|
69
|
-
"MonId"
|
|
70
|
-
"MonCotiz"
|
|
71
|
-
"ImpOpEx"
|
|
72
|
-
"ImpTrib"
|
|
65
|
+
"FeDetReq" => { "FECAEDetRequest" => { "Concepto" => Snoopy::CONCEPTS[bill.concept],
|
|
66
|
+
"DocTipo" => Snoopy::DOCUMENTS[bill.document_type],
|
|
67
|
+
"CbteFch" => today,
|
|
68
|
+
"ImpTotConc" => 0.00,
|
|
69
|
+
"MonId" => Snoopy::CURRENCY[bill.currency][:code],
|
|
70
|
+
"MonCotiz" => bill.exchange_rate,
|
|
71
|
+
"ImpOpEx" => 0.00,
|
|
72
|
+
"ImpTrib" => 0.00,
|
|
73
|
+
"CondicionIVAReceptorId" => bill.receiver_iva_condition_id }}}}
|
|
73
74
|
|
|
74
75
|
unless bill.issuer_iva_cond.to_sym == Snoopy::RESPONSABLE_MONOTRIBUTO
|
|
75
76
|
_alicivas = bill.alicivas.collect do |aliciva|
|
|
@@ -113,7 +114,7 @@ module Snoopy
|
|
|
113
114
|
[obs].flatten.each { |ob| afip_observations[ob[:code]] = ob[:msg] }
|
|
114
115
|
end
|
|
115
116
|
rescue => e
|
|
116
|
-
errors
|
|
117
|
+
@errors[:observation_parser] = Snoopy::Exception::AuthorizeAdapter::ObservationParser.new(e.message, e.backtrace).message
|
|
117
118
|
end
|
|
118
119
|
|
|
119
120
|
def parse_events(fecae_events)
|
|
@@ -121,7 +122,7 @@ module Snoopy
|
|
|
121
122
|
[events].flatten.each { |event| afip_events[event[:code]] = event[:msg] }
|
|
122
123
|
end
|
|
123
124
|
rescue => e
|
|
124
|
-
errors
|
|
125
|
+
@errors[:events_parser] = Snoopy::Exception::AuthorizeAdapter::EventsParser.new(e.message, e.backtrace).message
|
|
125
126
|
end
|
|
126
127
|
|
|
127
128
|
def parse_errors(fecae_errors)
|
|
@@ -129,7 +130,7 @@ module Snoopy
|
|
|
129
130
|
[errores].flatten.each { |error| afip_errors[error[:code]] = error[:msg] }
|
|
130
131
|
end
|
|
131
132
|
rescue => e
|
|
132
|
-
errors
|
|
133
|
+
@errors[:error_parser] = Snoopy::Exception::AuthorizeAdapter::ErrorParser.new(e.message, e.backtrace).message
|
|
133
134
|
end
|
|
134
135
|
|
|
135
136
|
def parse_fecae_solicitar_response
|
|
@@ -153,7 +154,7 @@ module Snoopy
|
|
|
153
154
|
parse_errors(fecae_result[:errors]) if fecae_result.has_key? :errors
|
|
154
155
|
parse_events(fecae_result[:events]) if fecae_result.has_key? :events
|
|
155
156
|
rescue => e
|
|
156
|
-
@errors
|
|
157
|
+
@errors[:fecae_response_parser] = Snoopy::Exception::AuthorizeAdapter::FecaeResponseParser.new(e.message, e.backtrace).message
|
|
157
158
|
end
|
|
158
159
|
end
|
|
159
160
|
|
|
@@ -178,7 +179,7 @@ module Snoopy
|
|
|
178
179
|
self.parse_errors(fe_comp_consultar_result[:errors]) if fe_comp_consultar_result and fe_comp_consultar_result.has_key? :errors
|
|
179
180
|
self.parse_events(fe_comp_consultar_result[:events]) if fe_comp_consultar_result and fe_comp_consultar_result.has_key? :events
|
|
180
181
|
rescue => e
|
|
181
|
-
@errors
|
|
182
|
+
@errors[:fecomp_consult_response_parser] = Snoopy::Exception::AuthorizeAdapter::FecompConsultResponseParser.new(e.message, e.backtrace).message
|
|
182
183
|
end
|
|
183
184
|
|
|
184
185
|
def client_configuration
|
data/lib/snoopy_afip/bill.rb
CHANGED
|
@@ -5,11 +5,11 @@ module Snoopy
|
|
|
5
5
|
:cbte_asoc_num, :cae, :service_date_to, :due_date,
|
|
6
6
|
:number, :alicivas, :cuit, :sale_point, :service_date_from,
|
|
7
7
|
:due_date_cae, :voucher_date, :process_date, :imp_iva, :cbte_asoc_to_sale_point,
|
|
8
|
-
:receiver_iva_cond, :issuer_iva_cond, :errors, :asoc_receiver_iva_cond]
|
|
8
|
+
:receiver_iva_cond, :issuer_iva_cond, :errors, :asoc_receiver_iva_cond, :receiver_iva_condition]
|
|
9
9
|
|
|
10
10
|
TAX_ATTRIBUTES = [:id, :amount, :taxeable_base]
|
|
11
11
|
|
|
12
|
-
ATTRIBUTES_PRECENSE = [:total_net, :concept, :receiver_iva_cond, :alicivas, :document_type, :service_date_from, :service_date_to, :sale_point, :issuer_iva_cond]
|
|
12
|
+
ATTRIBUTES_PRECENSE = [:total_net, :concept, :receiver_iva_cond, :alicivas, :document_type, :service_date_from, :service_date_to, :sale_point, :issuer_iva_cond, :receiver_iva_condition]
|
|
13
13
|
|
|
14
14
|
attr_accessor *ATTRIBUTES
|
|
15
15
|
|
|
@@ -31,9 +31,10 @@ module Snoopy
|
|
|
31
31
|
@issuer_iva_cond = attrs[:issuer_iva_cond]
|
|
32
32
|
@service_date_to = attrs[:service_date_to]
|
|
33
33
|
@service_date_from = attrs[:service_date_from]
|
|
34
|
-
@receiver_iva_cond = attrs[:receiver_iva_cond]
|
|
34
|
+
@receiver_iva_cond = attrs[:receiver_iva_cond] # Esto se usa para el cbte_type
|
|
35
35
|
@asoc_receiver_iva_cond = attrs[:asoc_receiver_iva_cond]
|
|
36
36
|
@cbte_asoc_to_sale_point = attrs[:cbte_asoc_to_sale_point] # Esto es el punto de venta de la factura para la nota de credito
|
|
37
|
+
@receiver_iva_condition = attrs[:receiver_iva_condition] # La verdadera condicion de iva del receptor
|
|
37
38
|
end
|
|
38
39
|
|
|
39
40
|
# def self.bill_request(number, attrs={})
|
|
@@ -105,6 +106,12 @@ module Snoopy
|
|
|
105
106
|
end
|
|
106
107
|
alias :to_hash :to_h
|
|
107
108
|
|
|
109
|
+
def receiver_iva_condition_id
|
|
110
|
+
return '' if @receiver_iva_condition.nil?
|
|
111
|
+
|
|
112
|
+
Snoopy::IVA_COND_RECEIVER[@receiver_iva_condition.to_sym]
|
|
113
|
+
end
|
|
114
|
+
|
|
108
115
|
private
|
|
109
116
|
|
|
110
117
|
def validate!
|
|
@@ -147,6 +154,12 @@ module Snoopy
|
|
|
147
154
|
status = false unless errors.empty?
|
|
148
155
|
end
|
|
149
156
|
|
|
157
|
+
unless Snoopy::IVA_COND_RECEIVER.keys.include?(@receiver_iva_condition&.to_sym)
|
|
158
|
+
@errors[:receiver_iva_condition] = [] unless errors.has_key?(:receiver_iva_condition)
|
|
159
|
+
@errors[:receiver_iva_condition] << Snoopy::Exception::Bill::InvalidValueAttribute.new("Invalid value #{@receiver_iva_condition}. Possible values #{Snoopy::IVA_COND_RECEIVER.keys}").message
|
|
160
|
+
status = false unless errors.empty?
|
|
161
|
+
end
|
|
162
|
+
|
|
150
163
|
unless Snoopy::DOCUMENTS.keys.include?(@document_type)
|
|
151
164
|
@errors[:document_type] = [] unless errors.has_key?(:document_type)
|
|
152
165
|
@errors[:document_type] << Snoopy::Exception::Bill::InvalidValueAttribute.new("Invalid value #{@document_type}. Possible values #{Snoopy::DOCUMENTS.keys}").message
|
|
@@ -25,9 +25,22 @@ module Snoopy
|
|
|
25
25
|
|
|
26
26
|
RESPONSABLE_INSCRIPTO = :responsable_inscripto
|
|
27
27
|
RESPONSABLE_MONOTRIBUTO = :responsable_monotributo
|
|
28
|
+
CONSUMIDOR_FINAL = :consumidor_final
|
|
29
|
+
IVA_SUJETO_EXENTO = :sujeto_exento
|
|
30
|
+
IVA_NO_ALCANZADO = :no_alcanzado
|
|
28
31
|
|
|
32
|
+
# Issuer
|
|
29
33
|
IVA_COND = [ RESPONSABLE_MONOTRIBUTO, RESPONSABLE_INSCRIPTO ]
|
|
30
34
|
|
|
35
|
+
# Receiver
|
|
36
|
+
IVA_COND_RECEIVER = {
|
|
37
|
+
RESPONSABLE_INSCRIPTO => '1', # registered_vat_payer
|
|
38
|
+
RESPONSABLE_MONOTRIBUTO => '6', # self_employed_taxpayer
|
|
39
|
+
CONSUMIDOR_FINAL => '5', # final_consumer
|
|
40
|
+
IVA_SUJETO_EXENTO => '4', # exempt
|
|
41
|
+
IVA_NO_ALCANZADO => '15' # not_applicable
|
|
42
|
+
}.freeze
|
|
43
|
+
|
|
31
44
|
CONCEPTS = { 'Productos' => '01',
|
|
32
45
|
'Servicios' => '02',
|
|
33
46
|
'Productos y Servicios' => '03' }
|
|
@@ -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
|
data/lib/snoopy_afip/version.rb
CHANGED
data/lib/snoopy_afip.rb
CHANGED
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.
|
|
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
|
|
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
|