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 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