snoopy_afip 2.1.1 → 3.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -7
- data/CHANGELOG +23 -23
- data/README.textile +236 -0
- data/lib/snoopy_afip/authentication_adapter.rb +119 -0
- data/lib/snoopy_afip/authorize_adapter.rb +178 -0
- data/lib/snoopy_afip/bill.rb +124 -203
- data/lib/snoopy_afip/client.rb +20 -0
- data/lib/snoopy_afip/constants.rb +69 -65
- data/lib/snoopy_afip/core_ext/hash.rb +11 -0
- data/lib/snoopy_afip/exceptions.rb +74 -0
- data/lib/snoopy_afip/version.rb +1 -1
- data/lib/snoopy_afip.rb +10 -10
- data/snoopy_afip.gemspec +7 -7
- metadata +74 -89
- data/lib/snoopy_afip/auth_data.rb +0 -46
- data/lib/snoopy_afip/authorizer.rb +0 -10
- data/spec/snoopy_afip/auth_data_spec.rb +0 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
|
-
---
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
1
|
+
---
|
2
|
+
SHA512:
|
3
|
+
data.tar.gz: cc38b0368056ee06c4dc5356d4d28266f85b398dd213255594ca2a1abbf18bab472707a1e530823c5d609d2243d4c89de375584719bbc209ee68972309eea1e9
|
4
|
+
metadata.gz: 8ebd13756ee6fc97af51cc5bcea676c059e117e2fa5cc1695a0438d469daa8003aa19975d778ad88a9fcea9204d57d31cb7c2d6b9471c58a588b6335c3c30650
|
5
|
+
SHA1:
|
6
|
+
data.tar.gz: 5e690eecae91ab18eae8e224f63c96ffdf883da8
|
7
|
+
metadata.gz: 6ba0173aef930a56479a87d58e60ac13708090c9
|
data/CHANGELOG
CHANGED
@@ -1,23 +1,23 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
1
|
+
# Changelog
|
2
|
+
All notable changes to this project will be documented in this file.
|
3
|
+
|
4
|
+
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
5
|
+
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
6
|
+
|
7
|
+
## [3.0.0] - June 29, 2017
|
8
|
+
### Added
|
9
|
+
- `Authentication Class` destinada a la comunicación con el **WSAA**.
|
10
|
+
- `Authorize Class` destinada a la comunicación con el **WSFE**.
|
11
|
+
- `Client Class` destinada para crear el cliente de `Savon`.
|
12
|
+
- Mejor manejo de Exceptions.
|
13
|
+
- Validaciones en el modelo `Bill`.
|
14
|
+
|
15
|
+
### Changed
|
16
|
+
- Se pasó toda la logica de comunicación con el **WSFE** del modelo `Bill` al modelo `Authorize`.
|
17
|
+
- No se crea mas el rchivo para la clave privada, devuelve en RAW.
|
18
|
+
- No se crea mas un archivo para almacenar el _token_, _sign_, se devuelve en RAW.
|
19
|
+
- Evitar raisear si se logró autorizar una factura con el **WSFE**. Manejo en el errors del `Bill` o el `Authorize`.
|
20
|
+
|
21
|
+
### Removed
|
22
|
+
- Eliminiado modulo `AuthData`.
|
23
|
+
- Eliminado el uso de `Bash` para autenticar en el **WSAA**.
|
data/README.textile
CHANGED
@@ -0,0 +1,236 @@
|
|
1
|
+
# snoopy_afip
|
2
|
+
conexión con Web Service de Factura Electrónica de AFIP (WSFE)
|
3
|
+
|
4
|
+
## Instalación
|
5
|
+
|
6
|
+
Añadir esta linea en el Gemfile:
|
7
|
+
|
8
|
+
```ruby
|
9
|
+
gem 'snoopy_afip'
|
10
|
+
```
|
11
|
+
|
12
|
+
Luego ejecuta:
|
13
|
+
|
14
|
+
$ bundle
|
15
|
+
|
16
|
+
O instala la gema a mano:
|
17
|
+
|
18
|
+
$ gem install snoopy_afip
|
19
|
+
|
20
|
+
### Versiones
|
21
|
+
|
22
|
+
- `2.1.1`. Usada en producción. (**Stable**)
|
23
|
+
- `3.0.0`. Aun en entorono de testing. (**Beta**)
|
24
|
+
|
25
|
+
## Antes que nada
|
26
|
+
|
27
|
+
* Link con el manual para desarrolladores.
|
28
|
+
|
29
|
+
- [Especificación Técnica WSAA]('http://www.afip.gov.ar/ws/WSAA/Especificacion_Tecnica_WSAA_1.2.0.pdf'): Especificación técnica para la comunicación con el **WSAA** (servicio de autenticación), su propósito es solicitar un _tocken_ y _sign_ para poder emitir facturas con el servicio de **WSFE** (Servicio de autorización de facturas)
|
30
|
+
|
31
|
+
- [Manual desarrollador](http://www.afip.gob.ar/fe/documentos/manual_desarrollador_COMPG_v2_9.pdf): Especificación técnica para la comunicación con **WSFE** (Servicio de autorización de facturas).
|
32
|
+
|
33
|
+
* Es recomendable descargar los **Wsdl** para evitar tener que pedirlo en cada request
|
34
|
+
|
35
|
+
- Wsdl **WSAA** (servicio de autenticación)
|
36
|
+
|
37
|
+
- Testing: [https://wsaahomo.afip.gov.ar/ws/services/LoginCms?wsdl]('https://wsaahomo.afip.gov.ar/ws/services/LoginCms?wsdl')
|
38
|
+
- Producción: [https://wsaa.afip.gov.ar/ws/services/LoginCms?wsdl]('https://wsaa.afip.gov.ar/ws/services/LoginCms?wsdl')
|
39
|
+
|
40
|
+
- **WSFE** (Servicio de autorización de facturas)
|
41
|
+
|
42
|
+
- Testing: [https://wswhomo.afip.gov.ar/wsfev1/service.asmx?wsdl]('https://wswhomo.afip.gov.ar/wsfev1/service.asmx?wsdl')
|
43
|
+
- Producción: [https://servicios1.afip.gov.ar/wsfev1/service.asmx?WSDL]('https://servicios1.afip.gov.ar/wsfev1/service.asmx?WSDL')
|
44
|
+
|
45
|
+
* Explicación detallada de los pasos a seguir para obtener el certificado desde el sitio web AFIP para emitir facturas electrónicas.
|
46
|
+
|
47
|
+
[obtención de certificado](https://www.afip.gob.ar/ws/WSAA/WSAA.ObtenerCertificado.pdf)
|
48
|
+
|
49
|
+
## USO
|
50
|
+
|
51
|
+
### Inicialización de parametros generales
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
Snoopy.default_currency = :peso
|
55
|
+
Snoopy.default_concept = 'Servicios'
|
56
|
+
Snoopy.default_document_type = 'CUIT' || 'DNI' # O alguna key de Snoopy::DOCUMENTS
|
57
|
+
# Para el caso de produccion
|
58
|
+
Snoopy.auth_url = 'https://wsaa.afip.gov.ar/ws/services/LoginCms?wsdl' || PATH_WSAA_PROP_WSDL
|
59
|
+
Snoopy.service_url = 'https://servicios1.afip.gov.ar/wsfev1/service.asmx?WSDL' || PATH_WSFE_PROP_WSDL
|
60
|
+
# Para el caso de desarrollo
|
61
|
+
Snoopy.auth_url = 'https://wsaahomo.afip.gov.ar/ws/services/LoginCms?wsdl' || PATH_WSAA_TEST_WSDL
|
62
|
+
Snoopy.service_url = 'https://wswhomo.afip.gov.ar/wsfev1/service.asmx?wsdl' || PATH_WSFE_TEST_WSDL
|
63
|
+
```
|
64
|
+
|
65
|
+
En caso de trabajar con `Ruby on Rails`, es recomendable crear un archivo con esta conf en `config/initializers/snoopy.rb`.
|
66
|
+
|
67
|
+
### Generar Clave privada
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
Snoopy::AuthenticationAdapter.generate_pkey(2048) # Si no se pasa argumento se generar una de 8192
|
71
|
+
```
|
72
|
+
|
73
|
+
Este metodo retorna el `RAW` de la pkey, la cual debera guardarse en algun archivo o alguna base de datos.
|
74
|
+
|
75
|
+
### Generar solicitud de pedido de certificado
|
76
|
+
|
77
|
+
#### With ruby
|
78
|
+
|
79
|
+
Este metodo aun no ha sido testeado, por lo que agredezco si alguien logra probarlo.
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
Snoopy::AuthenticationAdapter.generate_certificate_request_with_ruby(pkey_path, subj_o, subj_cn, subj_cuit)
|
83
|
+
```
|
84
|
+
|
85
|
+
#### With bash
|
86
|
+
|
87
|
+
Mantiene la generación de versiones pasadas de `Snoopy`
|
88
|
+
|
89
|
+
```bash
|
90
|
+
Snoopy::AuthenticationAdapter.generate_certificate_request_with_bash(pkey_path, subj_o, subj_cn, subj_cuit)
|
91
|
+
```
|
92
|
+
|
93
|
+
- `pkey_path`: Ruta absoluta de la llave privada.
|
94
|
+
- `subj_o`: Nombre de la compañia.
|
95
|
+
- `subj_cn`: Hostname del server que generará las solicitudes. En ruby se obtiene con `%x(hostname).chomp`
|
96
|
+
- `subj_cuit`: Cuit registrado en la AFIP de la compañia que emita facturas.
|
97
|
+
|
98
|
+
Una vez generado este archivo debe hacerse el tramite en el sitio web de la **AFIP** para obtener el certificado que permitirá autorizar facturas al webservice.
|
99
|
+
|
100
|
+
### Solicitar autorización para la emisión de facturas
|
101
|
+
|
102
|
+
Para poder emitir o autorizar facturas en el web service de la **AFIP** es necesario solicitar autorización.
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
# pkey_path: Ruta absoluta de la llave privada.
|
106
|
+
# cert_path: Ruta absoluta del cetificado obtendio del sitio oficial de la **AFIP**.
|
107
|
+
authentication_adapter = Snoopy::AuthenticationAdapter.new(pkey_path, pkey_cert)
|
108
|
+
authentication_adapter.authenticate!
|
109
|
+
```
|
110
|
+
|
111
|
+
`authentication_adapter.authenticate!` deberá retornar un `Hash` con las siguientes keys: `:token`, `:sign` y `:expiration_time`
|
112
|
+
|
113
|
+
La key `:expiration_time` determina hasta cuando se podrá autorizar facturas, el tiempo esta prefijado por **AFIP** y es el de 24 Horas desde el momento de la solicitud del **token sign**, superado este tiempo deberá solicitarse nuevamente un **token sign**.
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
# Response example
|
117
|
+
{ token: "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9InllcyI/Pgo8c3NvIHZlcnNpb249IjIuMCI+CiAgICA8aWQgdW5pcXVlX2lkPSI5MDIxNDczOTEiIHNyYz0iQ049d3NhYWhvbW8sIE89QUZJUCwgQz1BUiwgU0VSSUFMTlVNQkVSPUNVSVQgMzM2OTM0NTAyMzkiIGdlbl90aW1lPSIxNDk4NDk0NDQ1IiBleHBfdGltZT0iMTQ5ODUzNzcwNSIgZHN0PSJDTj13c2ZlLCBPPUFGSVAsIEM9QVIiLz4KICAgIDxvcGVyYXRpb24gdmFsdWU9ImdyYW50ZWQiIHR5cGU9ImxvZ2luIj4KICAgICAgICA8bG9naW4gdWlkPSJDPWFyLCBPPXNlcXVyZSBzYSwgU0VSSUFMTlVNQkVSPUNVSVQgMjAyNDE2MDgxNjcsIENOPXBjIiBzZXJ2aWNlPSJ3c2ZlIiByZWdtZXRob2Q9IjIyIiBlbnRpdHk9IjMzNjkzNDUwMjM5IiBhdXRobWV0aG9kPSJjbXMiPgogICAgICAgICAgICA8cmVsYXRpb25zPgogICAgICAgICAgICAgICAgPHJlbGF0aW9uIHJlbHR5cGU9IjQiIGtleT0iMjAyNDE2MDgxNjciLz4KICAgICAgICAgICAgPC9yZWxhdGlvbnM+CiAgICAgICAgPC9sb2dpbj4KICAgIDwvb3BlcmF0aW9uPgo8L3Nzbz4KCg==",
|
118
|
+
sign: "iSpp/5qxQntuzOQcqs6GlShFaOKtEagLY17TFDwMTiErquT/fEw5ki9Ff4RYGndc/49UGmUTnVjUqB0mxuJk2IG4t+J4AAyVsY6+xiBGvMXAM/5sAI78NDl7ibMxAcdPi+nBIrdydp5DLy2SB4u/G46kguc6+srBp2fo20f/+wM=",
|
119
|
+
expiration_time: #<DateTime: 2017-06-27T01:28:25-03:00 ((2457932j,16105s,963000000n),-10800s,2299161j)>}}
|
120
|
+
```
|
121
|
+
|
122
|
+
En caso de producirse algun tipo de error este será devuelto a traves de raise exception. El error mas comun puede deberse al que certificado obtenido a treves del pedido de cetificado no sea correcto.
|
123
|
+
|
124
|
+
### Autorizar factura
|
125
|
+
|
126
|
+
#### Crear `Bill`
|
127
|
+
```ruby
|
128
|
+
Snoopy::Bill.new(attrs)
|
129
|
+
|
130
|
+
```
|
131
|
+
Donde `attrs` debe estar conformado de la siguiente manera:
|
132
|
+
* `attrs[:cuit]` CUIT del emisor de la factura.
|
133
|
+
* `attrs[:concept]` Concepto de la factura, por defecto `Snoopy.default_concept`.
|
134
|
+
* `attrs[:imp_iva]` Monto total de impuestos.
|
135
|
+
* `attrs[:currency]` Tipo de moneda a utilizar, por default `Snoopy.default_currency`.
|
136
|
+
* `attrs[:alicivas]` Impuestos asociados a la factura.
|
137
|
+
* `attrs[:total_net]` Monto neto por defecto es 0.
|
138
|
+
* `attrs[:sale_point]` Punto de venta del emisor de la factura.
|
139
|
+
* `attrs[:document_type]` Tipo de documento a utilizar, por default `Snoopy.default_document`. Valores posibles `Snoopy::DOCUMENTS`
|
140
|
+
* `attrs[:document_num]` Numero de documento o cuit, el valor depende de `attrs[:document_type]`.
|
141
|
+
* `attrs[:issuer_iva_cond]` Condición de IVA del emisor de la factura. [`Snoopy::RESPONSABLE_INSCRIPTO` o `Snoopy::RESPONSABLE_MONOTRIBUTO`]
|
142
|
+
* `attrs[:receiver_iva_cond]` valores posibles `Snoopy::BILL_TYPE`
|
143
|
+
* `attrs[:service_date_from]` Inicio de vigencia de la factura.
|
144
|
+
* `attrs[:service_date_to]` Fin de vigencia de la factura.
|
145
|
+
* `attrs[:cbte_asoc_num]` Numero de la factura a la cual se quiere crear una nota de crédito (Solo pasar si se esta creando una nota de crédito).
|
146
|
+
* `attrs[:cbte_asoc_to_sale_point]` Punto de venta de la factura a la cual se quiere crear una nota de crédito (Solo pasar si se esta creando una nota de crédito).
|
147
|
+
|
148
|
+
El `attrs[:alicivas]` discrimina la información de los items. Es posible que en la factura se discriminen diferentes items con diferentes impuestos. Para ello el `attrs[:alicivas]` es un arreglo de `Hash`. Donde cada uno de ellos tiene la información sobre un determinado impuesto.
|
149
|
+
|
150
|
+
```ruby
|
151
|
+
{ id: tax_rate.round_with_precision(2), # Porcentaje. Ej: "0.105", "0.21", "0.27"
|
152
|
+
amount: tax_amount.round_with_precision(2), # Monto total del impuesto del item.
|
153
|
+
taxeable_base: net_amount.round_with_precision(2) } # Monto total del item sin considerar el impuesto.
|
154
|
+
```
|
155
|
+
|
156
|
+
Por ejemplo si se tiene 5 items, donde 3 de ellos tienen un impuesto del 10.5% y los 2 restantes del 21%. Los primeros 3 deben, se debera de crear dos hashes de la siguientes forma.
|
157
|
+
|
158
|
+
```ruby
|
159
|
+
attrs[:alicivas] = [ { id: (10.5 / 100 ).to_s
|
160
|
+
amount: X, # De los 3 items de 10.5
|
161
|
+
taxeable_base: Y }, # De los 3 items de 10.5
|
162
|
+
{ id: (21 / 100 ).to_s
|
163
|
+
amount: X, # De los 2 items de 21
|
164
|
+
taxeable_base: Y } ] # De los 2 items de 21
|
165
|
+
```
|
166
|
+
Donde:
|
167
|
+
|
168
|
+
* `X`: Es la parte del monto que le corresponde al impuesto.
|
169
|
+
* `Y`: Es la partes del monto sin impuesto.
|
170
|
+
|
171
|
+
Los `tax_rates` soportados por AFIP son los siguientes:
|
172
|
+
|
173
|
+
```ruby
|
174
|
+
Snoopy::ALIC_IVA
|
175
|
+
```
|
176
|
+
|
177
|
+
* Tips:
|
178
|
+
|
179
|
+
El `taxeable_base` se calcula de la siguiente manera:
|
180
|
+
|
181
|
+
```ruby
|
182
|
+
total_amount / (1 + tax_percentage)
|
183
|
+
```
|
184
|
+
|
185
|
+
- Metodos de interes
|
186
|
+
- **valid?** valida si la `bill` cumple lo minimo indispensable.
|
187
|
+
- **partial_approved?** si fue _Aprobada parcialmente_ por el webserice.
|
188
|
+
- **rejected?** si fue _Rechazada_ por el webservice.
|
189
|
+
- **approved?** si fue _Aprobada_ por el webservice.
|
190
|
+
- **errors** errores presentados durante la validación `valid?`
|
191
|
+
|
192
|
+
#### Autorizar el `Bill`
|
193
|
+
```ruby
|
194
|
+
authorize_adapter = Snoopy::AuthorizeAdapter.new({ bill: bill, # Obtenido en el paso anterior
|
195
|
+
pkey: pkey, # PATH de la clave privada generada anteriormente.
|
196
|
+
cert: certificate, # PATH del certificado descargado del sitio web de la AFIP.
|
197
|
+
cuit: cuit, # CUIT del emisor de la factura, mismo con el que se hizo el tramite.
|
198
|
+
sign: sign, # SIGN obtenido en la autenticación
|
199
|
+
token: token}) # TOKEN obtenido en la autenticación
|
200
|
+
|
201
|
+
authorize_adapter.authorize!
|
202
|
+
```
|
203
|
+
|
204
|
+
- Metodos de interes del `Snoopy::AuthorizeAdapter`
|
205
|
+
|
206
|
+
- **request** Información enviada al webservice de **AFIP**
|
207
|
+
- **response** Respuesta completa obtenida del webservice de **AFIP**
|
208
|
+
- **afip_errors** Errores parseados de la **response**. Pueden presentarse los motivos por los que no se autorizo la `bill`
|
209
|
+
- **afip_events** Eventos parseados de la **response** . Generalmente la **AFIP** los usa para informar de posibles cambios.
|
210
|
+
- **afip_observations** Observaciones parseados de la **response**. Pueden presentarse los motivos por los que no se autorizo la `bill`
|
211
|
+
- **errors** Errores presentados en el proceso de autorización. Explicado en el apartado siguiente.
|
212
|
+
|
213
|
+
### Manejo de excepciones
|
214
|
+
|
215
|
+
#### Explota
|
216
|
+
|
217
|
+
Hay errores que producirán que `Snoopy` generé un `raise`, estos son los casos que se produzcan errores previo o durante la comunicación con la **AFIP**. En estos casos se produce un `raise` debido a que no se logro la comunicación con la **AFIP** por lo que se puede asegurar que no se autorizo la `bill`
|
218
|
+
|
219
|
+
#### No Explota
|
220
|
+
Existe otro caso que se produzca un error posterior a la comunicación con el webservice, en este caso se pudo obtener una respuesta , pero se producierón errores en el parseo de la misma (Por ejemplo **AFIP** cambia el formato de la response), para ellos se puede consultar las exceptiones generadas por los parseadores con `authorize_adapter.errors`.
|
221
|
+
|
222
|
+
Hay 4 nivel de parser:
|
223
|
+
- **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`
|
224
|
+
- **Parseo de errores**: Parsea los errores retornados por la **AFIP**.
|
225
|
+
- **Parseo de eventos**: Parsea los eventos retornados por la **AFIP**.
|
226
|
+
- **Parseo de observaciones**: Parsea las observaciones retornados por la **AFIP**.
|
227
|
+
|
228
|
+
Implemente esto de esta manera debido a que sucedio que **AFIP** cambio algo en la estructura de los eventos, y al tener todo en un solo parser explotaba todo y no sabia que habia sucedido con la autorización de la factura.
|
229
|
+
|
230
|
+
## TO DO
|
231
|
+
- **Mejor parseo de los errores obtenidos en AFIP**: La idea seria poder identificar cada uno de los errores posibles obtenido, y agregar los errores en la `bill` al atributo que corresponda.
|
232
|
+
- **Batch**: Permitir a `Snoopy::AuthorizeAdapter` autorizar un pool de `Snoopy::Bill` en una sola request.
|
233
|
+
|
234
|
+
## License
|
235
|
+
|
236
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
module Snoopy
|
3
|
+
class AuthenticationAdapter
|
4
|
+
attr_reader :id, :from, :to, :pkey, :cert, :tra, :cms, :request, :response, :client
|
5
|
+
|
6
|
+
def initialize(attrs={})
|
7
|
+
time = Time.new
|
8
|
+
@id = time.to_i
|
9
|
+
@from = time.strftime("%Y-%m-%dT%H:%M:%S%:z")
|
10
|
+
@to = (time + 86400/2).strftime("%Y-%m-%dT%H:%M:%S%:z") # 86400.seg = 1.day
|
11
|
+
@pkey = attrs[:pkey]
|
12
|
+
@cert = attrs[:cert]
|
13
|
+
@client = Snoopy::Client.new(client_configuration)
|
14
|
+
end
|
15
|
+
|
16
|
+
# http://www.afip.gov.ar/ws/WSAA/Especificacion_Tecnica_WSAA_1.2.0.pdf
|
17
|
+
# coe.notAuthorized: Computador no autorizado a acceder los servicio de AFIP
|
18
|
+
# cms.bad: El CMS no es valido
|
19
|
+
# cms.bad.base64: No se puede decodificar el BASE64
|
20
|
+
# cms.cert.notFound: No se ha encontrado certificado de firma en el CMS
|
21
|
+
# cms.sign.invalid: Firma inválida o algoritmo no soportado
|
22
|
+
# cms.cert.expired: Certificado expirado
|
23
|
+
# cms.cert.invalid: Certificado con fecha de generación posterior a la actual
|
24
|
+
# cms.cert.untrusted: Certificado no emitido por AC de confianza
|
25
|
+
# xml.bad: No se ha podido interpretar el XML contra el SCHEMA
|
26
|
+
# xml.source.invalid: El atributo 'source' no se corresponde con el DN del Certificad
|
27
|
+
# xml.destination.invalid: El atributo 'destination' no se corresponde con el DN del WSAA
|
28
|
+
# xml.version.notSupported: La versión del documento no es soportada
|
29
|
+
# xml.generationTime.invalid: El tiempo de generación es posterior a la hora actual o posee más de 24 horas de antiguedad
|
30
|
+
# xml.expirationTime.expired: El tiempo de expiración es inferior a la hora actual
|
31
|
+
# xml.expirationTime.invalid: El tiempo de expiración del documento es superior a 24 horas
|
32
|
+
# wsn.unavailable: El servicio al que se desea acceder se encuentra momentáneamente fuera de servicio
|
33
|
+
# wsn.notFound: Servicio informado inexistente
|
34
|
+
# wsaa.unavailable: El servicio de autenticación/autorización se encuentra momentáneamente fuera de servicio
|
35
|
+
# wsaa.internalError: No se ha podido procesar el requerimiento
|
36
|
+
def authenticate!
|
37
|
+
@response = client.call(:login_cms, :message => { :in0 => build_cms })
|
38
|
+
parser_response.deep_symbolize_keys
|
39
|
+
end
|
40
|
+
|
41
|
+
def parser_response
|
42
|
+
response_credentials.merge( 'expiration_time' => response_header["expirationTime"] )
|
43
|
+
end
|
44
|
+
|
45
|
+
def response_header
|
46
|
+
@_header_response ||= response_to_hash["loginTicketResponse"]["header"]
|
47
|
+
end
|
48
|
+
|
49
|
+
def response_credentials
|
50
|
+
@_credentials ||= response_to_hash["loginTicketResponse"]["credentials"]
|
51
|
+
end
|
52
|
+
|
53
|
+
def response_to_hash
|
54
|
+
@_response_to_hash ||= Nori.new.parse(response[:login_cms_response][:login_cms_return])
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.generate_pkey(leng=8192)
|
58
|
+
begin
|
59
|
+
OpenSSL::PKey::RSA.new(leng).to_pem # %x(openssl genrsa 8192)
|
60
|
+
rescue => e
|
61
|
+
raise "command fail: 'openssl genrsa 8192' error al generar pkey: #{e.message}, error: #{e.message}"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.generate_certificate_request_with_ruby(pkey, subj_o, subj_cn, subj_cuit)
|
66
|
+
options = [ ['C', 'AR', OpenSSL::ASN1::PRINTABLESTRING],
|
67
|
+
['O', subj_o, OpenSSL::ASN1::UTF8STRING],
|
68
|
+
['CN', subj_cn, OpenSSL::ASN1::UTF8STRING],
|
69
|
+
['serialNumber', "CUIT #{subj_cuit}", OpenSSL::ASN1::UTF8STRING] ]
|
70
|
+
key = OpenSSL::PKey::RSA.new(File.read(pkey))
|
71
|
+
|
72
|
+
request = OpenSSL::X509::Request.new
|
73
|
+
request.version = 0
|
74
|
+
request.subject = OpenSSL::X509::Name.new(options)
|
75
|
+
request.public_key = key.public_key
|
76
|
+
request.sign(key, OpenSSL::Digest::SHA256.new).to_pem
|
77
|
+
end
|
78
|
+
|
79
|
+
# pkey: clave privada generada por el metodo generate_pkey.
|
80
|
+
# subj_o: Nombre de la empresa, registrado en AFIP.
|
81
|
+
# subj_cn: hostname del servidor que realizara la comunicación con AFIP.
|
82
|
+
# subj_cuit: Cuit registado en AFIP.
|
83
|
+
def self.generate_certificate_request_with_bash(pkey, subj_o, subj_cn, subj_cuit)
|
84
|
+
begin
|
85
|
+
%x(openssl req -new -key #{pkey} -subj "/C=AR/O=#{subj_o}/CN=#{subj_cn}/serialNumber=CUIT #{subj_cuit}")
|
86
|
+
rescue => e
|
87
|
+
raise "command fail: openssl req -new -key #{pkey} -subj /C=AR/O=#{subj_o}/CN=#{subj_cn}/serialNumber=CUIT #{subj_cuit} -out #{out_path}, error: #{e.message}"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def build_tra
|
92
|
+
builder = Nokogiri::XML::Builder.new(:encoding => 'UTF-8') do |xml|
|
93
|
+
xml.loginTicketRequest('version' => '1.0') {
|
94
|
+
xml.header {
|
95
|
+
xml.uniqueId id
|
96
|
+
xml.generationTime from
|
97
|
+
xml.expirationTime to
|
98
|
+
}
|
99
|
+
xml.service "wsfe"
|
100
|
+
}
|
101
|
+
end
|
102
|
+
builder.to_xml
|
103
|
+
end
|
104
|
+
|
105
|
+
def build_cms
|
106
|
+
key = OpenSSL::PKey::RSA.new(File.read(pkey))
|
107
|
+
crt = OpenSSL::X509::Certificate.new(File.read(cert))
|
108
|
+
pkcs7 = OpenSSL::PKCS7::sign(crt, key, build_tra)
|
109
|
+
@cms = pkcs7.to_pem.lines.to_a[1..-2].join
|
110
|
+
end
|
111
|
+
|
112
|
+
def client_configuration
|
113
|
+
{ :wsdl => Snoopy.auth_url,
|
114
|
+
:ssl_version => :TLSv1,
|
115
|
+
:pretty_print_xml => true }
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
@@ -0,0 +1,178 @@
|
|
1
|
+
module Snoopy
|
2
|
+
class AuthorizeAdapter
|
3
|
+
|
4
|
+
attr_accessor :bill, :cuit, :sign, :pkey, :cert, :token, :errors, :request, :response, :afip_errors, :afip_events, :afip_observations, :client
|
5
|
+
|
6
|
+
def initialize(attrs)
|
7
|
+
@bill = attrs[:bill]
|
8
|
+
@cuit = attrs[:cuit]
|
9
|
+
@sign = attrs[:sign]
|
10
|
+
@pkey = attrs[:pkey]
|
11
|
+
@cert = attrs[:cert]
|
12
|
+
@token = attrs[:token]
|
13
|
+
@errors = {}
|
14
|
+
@request = nil
|
15
|
+
@response = nil
|
16
|
+
@afip_errors = {}
|
17
|
+
@afip_events = {}
|
18
|
+
@afip_observations = {}
|
19
|
+
@client = Snoopy::Client.new(client_configuration)
|
20
|
+
end
|
21
|
+
|
22
|
+
def auth
|
23
|
+
{ "Token" => token, "Sign" => sign, "Cuit" => cuit }
|
24
|
+
end
|
25
|
+
|
26
|
+
def authorize!
|
27
|
+
return false unless bill.valid?
|
28
|
+
set_bill_number!
|
29
|
+
build_body_request
|
30
|
+
@response = client.call( :fecae_solicitar, :message => @request )
|
31
|
+
parse_fecae_solicitar_response
|
32
|
+
!@response.nil?
|
33
|
+
end
|
34
|
+
|
35
|
+
def set_bill_number!
|
36
|
+
message = { "Auth" => auth, "PtoVta" => bill.sale_point, "CbteTipo" => bill.cbte_type }
|
37
|
+
resp = client.call( :fe_comp_ultimo_autorizado, :message => message )
|
38
|
+
|
39
|
+
begin
|
40
|
+
resp_errors = resp[:fe_comp_ultimo_autorizado_response][:fe_comp_ultimo_autorizado_result][:errors]
|
41
|
+
resp_errors.each_value { |value| errors[value[:code]] = value[:msg] } unless resp_errors.nil?
|
42
|
+
bill.number = resp[:fe_comp_ultimo_autorizado_response][:fe_comp_ultimo_autorizado_result][:cbte_nro].to_i + 1 if errors.empty?
|
43
|
+
rescue => e
|
44
|
+
raise Snoopy::Exception::AuthorizeAdapter::SetBillNumberParser.new(e.message, e.backtrace)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def build_body_request
|
49
|
+
# today = Time.new.in_time_zone('Buenos Aires').strftime('%Y%m%d')
|
50
|
+
today = Date.today.strftime('%Y%m%d')
|
51
|
+
fecaereq = {"FeCAEReq" => { "FeCabReq" => { "CantReg" => "1", "CbteTipo" => bill.cbte_type, "PtoVta" => bill.sale_point },
|
52
|
+
"FeDetReq" => { "FECAEDetRequest" => { "Concepto" => Snoopy::CONCEPTS[bill.concept],
|
53
|
+
"DocTipo" => Snoopy::DOCUMENTS[bill.document_type],
|
54
|
+
"CbteFch" => today,
|
55
|
+
"ImpTotConc" => 0.00,
|
56
|
+
"MonId" => Snoopy::CURRENCY[bill.currency][:code],
|
57
|
+
"MonCotiz" => bill.exchange_rate,
|
58
|
+
"ImpOpEx" => 0.00,
|
59
|
+
"ImpTrib" => 0.00 }}}}
|
60
|
+
|
61
|
+
unless bill.issuer_iva_cond.to_sym == Snoopy::RESPONSABLE_MONOTRIBUTO
|
62
|
+
_alicivas = bill.alicivas.collect do |aliciva|
|
63
|
+
{ 'Id' => Snoopy::ALIC_IVA[aliciva[:id]], 'Importe' => aliciva[:amount], 'BaseImp' => aliciva[:taxeable_base] }
|
64
|
+
end
|
65
|
+
fecaereq["FeCAEReq"]["FeDetReq"]["FECAEDetRequest"]["Iva"] = { "AlicIva" => _alicivas }
|
66
|
+
end
|
67
|
+
|
68
|
+
detail = fecaereq["FeCAEReq"]["FeDetReq"]["FECAEDetRequest"]
|
69
|
+
|
70
|
+
detail["DocNro"] = bill.document_num
|
71
|
+
detail["ImpNeto"] = bill.total_net.to_f
|
72
|
+
detail["ImpIVA"] = bill.iva_sum
|
73
|
+
detail["ImpTotal"] = bill.total
|
74
|
+
detail["CbteDesde"] = detail["CbteHasta"] = bill.number
|
75
|
+
|
76
|
+
unless bill.concept == "Productos"
|
77
|
+
detail.merge!({ "FchServDesde" => bill.service_date_from || today,
|
78
|
+
"FchServHasta" => bill.service_date_to || today,
|
79
|
+
"FchVtoPago" => bill.due_date || today})
|
80
|
+
end
|
81
|
+
|
82
|
+
if bill.receiver_iva_cond.to_s.include?("nota_credito")
|
83
|
+
detail.merge!({"CbtesAsoc" => {"CbteAsoc" => {"Nro" => bill.cbte_asoc_num,
|
84
|
+
"PtoVta" => bill.cbte_asoc_to_sale_point,
|
85
|
+
"Tipo" => bill.cbte_type }}})
|
86
|
+
end
|
87
|
+
|
88
|
+
@request = { "Auth" => auth }.merge!(fecaereq)
|
89
|
+
rescue => e
|
90
|
+
raise Snoopy::Exception::AuthorizeAdapter::BuildBodyRequest.new(e.message, e.backtrace)
|
91
|
+
end
|
92
|
+
|
93
|
+
def parse_observations(fecae_observations)
|
94
|
+
fecae_observations.each_value do |obs|
|
95
|
+
[obs].flatten.each { |ob| afip_observations[ob[:code]] = ob[:msg] }
|
96
|
+
end
|
97
|
+
rescue => e
|
98
|
+
errors << Snoopy::Exception::AuthorizeAdapter::ObservationParser.new(e.message, e.backtrace)
|
99
|
+
end
|
100
|
+
|
101
|
+
def parse_events(fecae_events)
|
102
|
+
fecae_events.each_value do |events|
|
103
|
+
[events].flatten.each { |event| afip_events[event[:code]] = event[:msg] }
|
104
|
+
end
|
105
|
+
rescue => e
|
106
|
+
errors << Snoopy::Exception::AuthorizeAdapter::EventsParser.new(e.message, e.backtrace)
|
107
|
+
end
|
108
|
+
|
109
|
+
def parse_errors(fecae_errors)
|
110
|
+
fecae_errors.each_value do |errores|
|
111
|
+
[errores].flatten.each { |error| afip_errors[error[:code]] = error[:msg] }
|
112
|
+
end
|
113
|
+
rescue => e
|
114
|
+
errors << Snoopy::Exception::AuthorizeAdapter::ErrorParser.new(e.message, e.backtrace)
|
115
|
+
end
|
116
|
+
|
117
|
+
def parse_fecae_solicitar_response
|
118
|
+
begin
|
119
|
+
fecae_result = response[:fecae_solicitar_response][:fecae_solicitar_result]
|
120
|
+
fecae_response = fecae_result[:fe_det_resp][:fecae_det_response]
|
121
|
+
|
122
|
+
bill.number = fecae_response[:cbte_desde]
|
123
|
+
bill.cae = fecae_response[:cae]
|
124
|
+
bill.due_date_cae = fecae_response[:cae_fch_vto]
|
125
|
+
bill.result = fecae_response[:resultado]
|
126
|
+
rescue => e
|
127
|
+
raise Snoopy::Exception::AuthorizeAdapter::FecaeSolicitarResultParser.new(e.message, e.backtrace)
|
128
|
+
end
|
129
|
+
|
130
|
+
begin
|
131
|
+
bill.voucher_date = fecae_response[:cbte_fch]
|
132
|
+
bill.process_date = fecae_result[:fe_cab_resp][:fch_proceso]
|
133
|
+
|
134
|
+
parse_observations(fecae_response.delete(:observaciones)) if fecae_response.has_key? :observaciones
|
135
|
+
parse_errors(fecae_result[:errors]) if fecae_result.has_key? :errors
|
136
|
+
parse_events(fecae_result[:events]) if fecae_result.has_key? :events
|
137
|
+
rescue => e
|
138
|
+
@errors << Snoopy::Exception::AuthorizeAdapter::FecaeResponseParser.new(e.message, e.backtrace)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def parse_fe_comp_consultar_response
|
143
|
+
fe_comp_consultar_result = response[:fe_comp_consultar_response][:fe_comp_consultar_result]
|
144
|
+
result_get = fe_comp_consultar_result[:result_get]
|
145
|
+
|
146
|
+
unless result_get.nil?
|
147
|
+
bill.result = result_get[:resultado]
|
148
|
+
bill.number = result_get[:cbte_desde]
|
149
|
+
bill.cae = result_get[:cod_autorizacion]
|
150
|
+
bill.due_date_cae = result_get[:fch_vto]
|
151
|
+
bill.imp_iva = result_get[:imp_iva]
|
152
|
+
bill.document_num = result_get[:doc_numero]
|
153
|
+
bill.process_date = result_get[:fch_proceso]
|
154
|
+
bill.voucher_date = result_get[:cbte_fch]
|
155
|
+
bill.service_date_to = result_get[:fch_serv_hasta]
|
156
|
+
bill.service_date_from = result_get[:fch_serv_desde]
|
157
|
+
parse_events(result_get[:observaciones]) if result_get.has_key? :observaciones
|
158
|
+
end
|
159
|
+
|
160
|
+
self.parse_events(fe_comp_consultar_result[:errors]) if fe_comp_consultar_result and fe_comp_consultar_result.has_key? :errors
|
161
|
+
self.parse_events(fe_comp_consultar_result[:events]) if fe_comp_consultar_result and fe_comp_consultar_result.has_key? :events
|
162
|
+
rescue => e
|
163
|
+
@errors << Snoopy::Exception::FecompConsultResponseParser.new(e.message, e.backtrace)
|
164
|
+
end
|
165
|
+
|
166
|
+
def client_configuration
|
167
|
+
{ :wsdl => Snoopy.service_url,
|
168
|
+
:headers => { "Accept-Encoding" => "gzip, deflate", "Connection" => "Keep-Alive" },
|
169
|
+
:namespaces => {"xmlns" => "http://ar.gov.afip.dif.FEV1/"},
|
170
|
+
:ssl_version => :TLSv1,
|
171
|
+
:read_timeout => 90,
|
172
|
+
:open_timeout => 90,
|
173
|
+
:ssl_cert_file => cert,
|
174
|
+
:ssl_cert_key_file => pkey,
|
175
|
+
:pretty_print_xml => true }
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|