snoopy_afip 2.1.1 → 3.0.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 CHANGED
@@ -1,7 +1,7 @@
1
- ---
2
- SHA1:
3
- metadata.gz: e1b6ea5c224ff75e7dc085b10b57acc2a93125b7
4
- data.tar.gz: 6c488db06f8c01ee165fa1c3ea45846b862148db
5
- SHA512:
6
- metadata.gz: 964dd0b5447d8bcd6a9d6ec5b290636002460b6069061a9527d8aefb2158c643d0fc31b54365ee2415750caf339680152850ba313d8e30579f64b66f6736566a
7
- data.tar.gz: b659f8fae3b64992d40cd607e3cff4b663eedbe95a43823fa61eeaa09551886df9fa30c8ed8084dcf04a44b49cb262db6bb83f258432015eb1e3eac8b1a05a20
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
- *Bravo 0.4.0 (March 21, 2011)*
2
-
3
- * Auth service URL is now part of the config [miloops]
4
-
5
- *Bravo 0.3.6 (March 14, 2011)*
6
-
7
- * Hash extensions play nice with Rails 2.3 ruby 1.9 [miloops]
8
-
9
- *Bravo 0.3.5 (March 11, 2011)* (in version 0.3.5 this is listed as 0.4.0)
10
-
11
- * Errors will be raised if CERT or PKEY are not present [miloops]
12
- * Removed various hardcoded options [leanucci]
13
- * Added support for more iva conditions [miloops]
14
- * Log verbosity switch [leanucci]
15
- * Better spec coverage
16
-
17
- *Bravo 0.3.0 (March 07, 2011)*
18
-
19
- * Bill#response returns the full list of parameters passed and returned by FECAESolicitar [leanucci]
20
-
21
- *Bravo 0.2.0 (March 04, 2011)*
22
-
23
- * Bill#response returns a complete hash from WSFE response [leanucci]
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