Afip 0.1.1 → 0.1.2

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
1
  ---
2
2
  SHA1:
3
- metadata.gz: c44d1855f1f1844a4745cce85cdd919db4286ef4
4
- data.tar.gz: 0b3146d5b0fd7718f7298426dd599ecdc8f33142
3
+ metadata.gz: 3940933ea4642917aef2e693aa7c170052d19d4b
4
+ data.tar.gz: 9766eff2f7d9e62c270d977f24e1b5c3967bf45d
5
5
  SHA512:
6
- metadata.gz: f8f08e2292ce4cbf8c93b434a365625fd32b65664fbaf17b906cd5ca9dd3edc78a6864ed369f9a84c5d9ca085baa78dab75a61daa9cf58e8ca2c126a78d49080
7
- data.tar.gz: 2984ec6b14ed59738607ae81498bcea3db05a66205e130de2bb7d0dcb4fd83e9616eeffaa0c303fe11ed7324a6e72780a7677ed2010af53dc30d967dc52c9d9b
6
+ metadata.gz: cbda0d40344b3637967d267cb3272a2c7a0e2ec5e074d471e45605da7a8dabdd5aabf7a781a34a99d7e5fdc014c3010ab8d835bac62a41179b4aec8b4dcc8860
7
+ data.tar.gz: e203ce591b376f75f54fa62ea5bc83e22a8e7aeec9ddf4916db2a8e51dad0ea7f935f091c4bac838bde9665af0777669c2828960edfa373cb0d19c028a9bf270
data/.DS_Store ADDED
Binary file
data/Afip-0.1.0.gem ADDED
Binary file
data/Gemfile.lock ADDED
@@ -0,0 +1,20 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ Afip (0.1.1)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ rake (10.5.0)
10
+
11
+ PLATFORMS
12
+ ruby
13
+
14
+ DEPENDENCIES
15
+ Afip!
16
+ bundler (~> 1.16)
17
+ rake (~> 10.0)
18
+
19
+ BUNDLED WITH
20
+ 1.16.2
data/lib/.DS_Store ADDED
Binary file
@@ -0,0 +1,69 @@
1
+ module Afip
2
+
3
+ # This class handles authorization data
4
+ #
5
+ class AuthData
6
+
7
+ class << self
8
+
9
+ attr_accessor :environment, :todays_data_file_name
10
+
11
+ # Fetches WSAA Authorization Data to build the datafile for the day.
12
+ # It requires the private key file and the certificate to exist and
13
+ # to be configured as Afip.pkey and Afip.cert
14
+ #
15
+ def fetch(service = "wsfe")
16
+ unless File.exists?(Afip.pkey)
17
+ raise "Archivo de llave privada no encontrado en #{ Afip.pkey }"
18
+ end
19
+
20
+ unless File.exists?(Afip.cert)
21
+ raise "Archivo certificado no encontrado en #{ Afip.cert }"
22
+ end
23
+
24
+ unless File.exists?(todays_data_file_name)
25
+ Afip::Wsaa.login(service)
26
+ end
27
+
28
+ YAML.load_file(todays_data_file_name).each do |k, v|
29
+ Afip.const_set(k.to_s.upcase, v) unless Afip.const_defined?(k.to_s.upcase)
30
+ end
31
+ end
32
+
33
+ # Returns the authorization hash, containing the Token, Signature and Cuit
34
+ # @return [Hash]
35
+ #
36
+ def auth_hash
37
+ fetch unless Afip.constants.include?(:TOKEN) && Afip.constants.include?(:SIGN)
38
+ case service
39
+ when "wsfe"
40
+ { 'Token' => Afip::TOKEN, 'Sign' => Afip::SIGN, 'Cuit' => Afip.cuit }
41
+ when "ws_sr_padron_a4"
42
+ { 'token' => Afip::TOKEN, 'sign' => Afip::SIGN, 'cuitRepresentado' => Afip.cuit }
43
+ end
44
+ end
45
+
46
+ # Returns the right wsaa url for the specific environment
47
+ # @return [String]
48
+ #
49
+ def wsaa_url
50
+ pp Afip::URLS[environment][:wsaa]
51
+ end
52
+
53
+ # Returns the right wsfe url for the specific environment
54
+ # @return [String]
55
+ #
56
+ def wsfe_url
57
+ raise 'Environment not sent to either :test or :production' unless Afip::URLS.keys.include? environment
58
+ Afip::URLS[environment][:wsfe]
59
+ end
60
+
61
+ # Creates the data file name for a cuit number and the current day
62
+ # @return [String]
63
+ #
64
+ def todays_data_file_name
65
+ @todays_data_file ||= "/tmp/#{environment.to_s}_Afip_#{ Afip.cuit }_#{ Time.new.strftime('%Y_%m_%d') }.yml"
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,10 @@
1
+ module Afip
2
+ class Authorizer
3
+ attr_reader :pkey, :cert
4
+
5
+ def initialize
6
+ @pkey = Afip.pkey
7
+ @cert = Afip.cert
8
+ end
9
+ end
10
+ end
data/lib/Afip/bill.rb ADDED
@@ -0,0 +1,178 @@
1
+ module Afip
2
+ class Bill
3
+ attr_reader :cbte_type, :body, :response, :fecha_emision, :total, :client
4
+ attr_accessor :net, :doc_num, :iva_cond, :documento, :concepto, :moneda,
5
+ :due_date, :fch_serv_desde, :fch_serv_hasta, :fch_emision,
6
+ :ivas, :sale_point
7
+
8
+ def initialize(attrs={})
9
+ set_client
10
+ @sale_point = attrs[:sale_point]
11
+ @body = { "Auth" => Afip.auth_hash }
12
+ @net = attrs[:net] || 0
13
+ @documento = attrs[:documento] || Afip.default_documento
14
+ @moneda = attrs[:moneda] || Afip.default_moneda
15
+ @iva_cond = attrs[:iva_cond]
16
+ @concepto = attrs[:concepto] || Afip.default_concepto
17
+ @ivas = attrs[:ivas] || Array.new # [ 1, 100.00, 10.50 ], [ 2, 100.00, 21.00 ]
18
+ @fecha_emision = attrs[:fch_emision] || Time.new
19
+ @cbte_type = Afip::BILL_TYPE[Afip.own_iva_cond][iva_cond] || raise(NullOrInvalidAttribute.new, "Please choose a valid document type.")
20
+ @total = net.zero? ? 0 : net + iva_sum
21
+ end
22
+
23
+ def set_client
24
+ Afip::AuthData.fetch
25
+ @client = Savon.client(
26
+ wsdl: Afip.service_url,
27
+ namespaces: { "xmlns" => "http://ar.gov.afip.dif.FEV1/" },
28
+ log_level: :debug,
29
+ ssl_cert_key_file: Afip.pkey,
30
+ ssl_cert_file: Afip.cert,
31
+ ssl_verify_mode: :none,
32
+ read_timeout: 90,
33
+ open_timeout: 90,
34
+ headers: { "Accept-Encoding" => "gzip, deflate", "Connection" => "Keep-Alive" }
35
+ )
36
+ end
37
+
38
+ def authorize
39
+ body = setup_bill
40
+ response = client.call(:fecae_solicitar, message: body)
41
+ setup_response(response.to_hash)
42
+ authorized?
43
+ end
44
+
45
+ def setup_bill
46
+ array_ivas = Array.new
47
+ ivas.each{ |i|
48
+ array_ivas << {
49
+ "Id" => Afip::ALIC_IVA[ i[0] ][0],
50
+ "BaseImp" => i[1] ,
51
+ "Importe" => i[2] }
52
+ }
53
+
54
+ fecaereq = {
55
+ "FeCAEReq" => {
56
+ "FeCabReq" => Afip::Bill.header(cbte_type, sale_point),
57
+ "FeDetReq" => {
58
+ "FECAEDetRequest" => {
59
+ "Concepto" => Afip::CONCEPTOS[concepto],
60
+ "DocTipo" => Afip::DOCUMENTOS[documento],
61
+ "DocNro" => doc_num,
62
+ "CbteFch" => fecha_emision.strftime('%Y%m%d'),
63
+ "ImpTotConc" => 0.00,
64
+ "ImpNeto" => net.to_f,
65
+ "MonId" => Afip::MONEDAS[moneda][:codigo],
66
+ "MonCotiz" => exchange_rate,
67
+ "ImpOpEx" => 0.00,
68
+ "ImpTrib" => 0.00,
69
+ "ImpTotal" => (Afip.own_iva_cond == :responsable_monotributo ? total : net).to_f.round(2),
70
+ "CbteDesde" => next_bill_number,
71
+ "CbteHasta" => next_bill_number
72
+ }
73
+ }
74
+ }
75
+ }
76
+
77
+ detail = fecaereq["FeCAEReq"]["FeDetReq"]["FECAEDetRequest"]
78
+
79
+ if (Afip.own_iva_cond == :responsable_monotributo)
80
+ detail["ImpIVA"] = iva_sum
81
+ detail["Iva"] = { "AlicIva" => array_ivas }
82
+ end
83
+
84
+ unless concepto == "Productos" # En "Productos" ("01"), si se mandan estos parámetros la afip rechaza.
85
+ detail.merge!({"FchServDesde" => fch_serv_desde,
86
+ "FchServHasta" => fch_serv_hasta,
87
+ "FchVtoPago" => due_date})
88
+ end
89
+
90
+ body.merge!(fecaereq)
91
+ end
92
+
93
+ def self.header(cbte_type, sale_point)
94
+ {"CantReg" => "1", "CbteTipo" => cbte_type, "PtoVta" => sale_point}
95
+ end
96
+
97
+ def exchange_rate
98
+ return 1 if moneda == :peso
99
+ response = client.call :fe_param_get_cotizacion do
100
+ message = body.merge!({"MonId" => Afip::MONEDAS[moneda][:codigo]})
101
+ end
102
+ response.to_hash[:fe_param_get_cotizacion_response][:fe_param_get_cotizacion_result][:result_get][:mon_cotiz].to_f
103
+ end
104
+
105
+ def iva_sum
106
+ iva_sum = 0.0
107
+ self.ivas.each{ |i|
108
+ iva_sum += i[1] * Afip::ALIC_IVA[ i[0] ][1]
109
+ }
110
+ return iva_sum.round(2)
111
+ end
112
+
113
+ def next_bill_number
114
+ var = {"Auth" => Afip.auth_hash, "PtoVta" => sale_point, "CbteTipo" => cbte_type}
115
+ resp = client.call :fe_comp_ultimo_autorizado do
116
+ message(var)
117
+ end
118
+
119
+ resp.to_hash[:fe_comp_ultimo_autorizado_response][:fe_comp_ultimo_autorizado_result][:cbte_nro].to_i + 1
120
+ end
121
+
122
+ def setup_response(response)
123
+ # TODO: turn this into an all-purpose Response class
124
+
125
+ result = response[:fecae_solicitar_response][:fecae_solicitar_result]
126
+
127
+ if not result[:fe_det_resp] or not result[:fe_cab_resp] then
128
+ # Si no obtuvo respuesta ni cabecera ni detalle, evito hacer '[]' sobre algo indefinido.
129
+ # Ejemplo: Error con el token-sign de WSAA
130
+ keys, values = {
131
+ :errores => result[:errors],
132
+ :header_result => {:resultado => "X" },
133
+ :observaciones => nil
134
+ }.to_a.transpose
135
+ @response = (defined?(Struct::ResponseMal) ? Struct::ResponseMal : Struct.new("ResponseMal", *keys)).new(*values)
136
+ return
137
+ end
138
+
139
+ response_header = result[:fe_cab_resp]
140
+ response_detail = result[:fe_det_resp][:fecae_det_response]
141
+
142
+ request_header = body["FeCAEReq"]["FeCabReq"].underscore_keys.symbolize_keys
143
+ request_detail = body["FeCAEReq"]["FeDetReq"]["FECAEDetRequest"].underscore_keys.symbolize_keys
144
+
145
+ # Esto no funciona desde que se soportan múltiples alícuotas de iva simultáneas
146
+ # FIX ? TO-DO
147
+ # iva = request_detail.delete(:iva)["AlicIva"].underscore_keys.symbolize_keys
148
+ # request_detail.merge!(iva)
149
+
150
+ if result[:errors] then
151
+ response_detail.merge!( result[:errors] )
152
+ end
153
+
154
+ response_hash = {
155
+ :header_result => response_header.delete(:resultado),
156
+ :authorized_on => response_header.delete(:fch_proceso),
157
+ :detail_result => response_detail.delete(:resultado),
158
+ :cae_due_date => response_detail.delete(:cae_fch_vto),
159
+ :cae => response_detail.delete(:cae),
160
+ :iva_id => request_detail.delete(:id),
161
+ :iva_importe => request_detail.delete(:importe),
162
+ :moneda => request_detail.delete(:mon_id),
163
+ :cotizacion => request_detail.delete(:mon_cotiz),
164
+ :iva_base_imp => request_detail.delete(:base_imp),
165
+ :doc_num => request_detail.delete(:doc_nro),
166
+ :observaciones => response_detail.delete(:observaciones),
167
+ :errores => response_detail.delete(:err)
168
+ }.merge!(request_header).merge!(request_detail)
169
+
170
+ keys, values = response_hash.to_a.transpose
171
+ @response = (defined?(Struct::Response) ? Struct::Response : Struct.new("Response", *keys)).new(*values)
172
+ end
173
+
174
+ def authorized?
175
+ !response.nil? && response.header_result == "A" && response.detail_result == "A"
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,63 @@
1
+ module Afip
2
+ CBTE_TIPO = {
3
+ "01"=>"Factura A",
4
+ "02"=>"Nota de Débito A",
5
+ "03"=>"Nota de Crédito A",
6
+ "06"=>"Factura B",
7
+ "07"=>"Nota de Debito B",
8
+ "08"=>"Nota de Credito B",
9
+ "11"=>"Factura C",
10
+ "12"=>"Nota de Debito C",
11
+ "13"=>"Nota de Credito C"
12
+ }
13
+
14
+ CONCEPTOS = {"Productos"=>"01", "Servicios"=>"02", "Productos y Servicios"=>"03"}
15
+
16
+ DOCUMENTOS = {"CUIT"=>"80", "CUIL"=>"86", "CDI"=>"87", "LE"=>"89", "LC"=>"90", "CI Extranjera"=>"91", "en tramite"=>"92", "Acta Nacimiento"=>"93", "CI Bs. As. RNP"=>"95", "DNI"=>"96", "Pasaporte"=>"94", "Doc. (Otro)"=>"99"}
17
+
18
+ MONEDAS = {
19
+ :peso => {:codigo => "PES", :nombre =>"Pesos Argentinos"},
20
+ :dolar => {:codigo => "DOL", :nombre =>"Dolar Estadounidense"},
21
+ :real => {:codigo => "012", :nombre =>"Real"},
22
+ :euro => {:codigo => "060", :nombre =>"Euro"},
23
+ :oro => {:codigo => "049", :nombre =>"Gramos de Oro Fino"}
24
+ }
25
+
26
+ ALIC_IVA = [["03", 0], ["04", 0.105], ["05", 0.21], ["06", 0.27]]
27
+
28
+ BILL_TYPE = {
29
+ :responsable_inscripto => {
30
+ :responsable_inscripto => "01",
31
+ :consumidor_final => "06",
32
+ :exento => "06",
33
+ :responsable_monotributo => "06",
34
+ :nota_credito_a => "03",
35
+ :nota_credito_b => "08",
36
+ :nota_debito_a => "02",
37
+ :nota_debito_b => "07"
38
+ },
39
+ :responsable_monotributo => {
40
+ :responsable_inscripto => "11",
41
+ :consumidor_final => "11",
42
+ :exento => "11",
43
+ :responsable_monotributo => "11",
44
+ :nota_credito_c => "13",
45
+ :nota_debito_c => "12"
46
+ }
47
+ }
48
+
49
+ URLS =
50
+ {
51
+ :test => {
52
+ :wsaa => 'https://wsaahomo.afip.gov.ar/ws/services/LoginCms',
53
+ :padron => "https://awshomo.afip.gov.ar/sr-padron/webservices/personaServiceA5?WSDL",
54
+ :wsfe => 'https://wswhomo.afip.gov.ar/wsfev1/service.asmx?WSDL'
55
+ },
56
+ :production => {
57
+ :wsaa => 'https://wsaa.afip.gov.ar/ws/services/LoginCms',
58
+ :padron => "https://aws.afip.gov.ar/sr-padron/webservices/personaServiceA5?WSDL",
59
+ :wsfe => 'https://servicios1.afip.gov.ar/wsfev1/service.asmx'
60
+ }
61
+ }
62
+
63
+ end
@@ -0,0 +1,8 @@
1
+ class Float
2
+ def round_with_precision(precision = nil)
3
+ precision.nil? ? round : (self * (10 ** precision)).round / (10 ** precision).to_f
4
+ end
5
+ def round_up_with_precision(precision = nil)
6
+ precision.nil? ? round : ((self * (10 ** precision)).round + 1) / (10 ** precision).to_f
7
+ end
8
+ end
@@ -0,0 +1,23 @@
1
+ class Hash
2
+ def symbolize_keys!
3
+ keys.each do |key|
4
+ self[(key.to_sym rescue key) || key] = delete(key)
5
+ end
6
+ self
7
+ end unless method_defined?(:symbolize_keys!)
8
+
9
+ def symbolize_keys
10
+ dup.symbolize_keys!
11
+ end unless method_defined?(:symbolize_keys)
12
+
13
+ def underscore_keys!
14
+ keys.each do |key|
15
+ self[(key.underscore rescue key) || key] = delete(key)
16
+ end
17
+ self
18
+ end unless method_defined?(:underscore_keys!)
19
+
20
+ def underscore_keys
21
+ dup.underscore_keys!
22
+ end unless method_defined?(:underscore_keys)
23
+ end
@@ -0,0 +1,12 @@
1
+ # Stolen from activesupport/lib/active_support/inflector/methods.rb, line 48
2
+ class String
3
+ def underscore
4
+ word = self.to_s.dup
5
+ word.gsub!(/::/, '/')
6
+ word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
7
+ word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
8
+ word.tr!("-", "_")
9
+ word.downcase!
10
+ word
11
+ end
12
+ end
@@ -0,0 +1,181 @@
1
+ module Afip
2
+ class Padron
3
+ attr_reader :client, :body, :fault_code, :data
4
+ attr_accessor :dni, :tipo
5
+ def initialize(attrs = {})
6
+ Afip::AuthData.environment = Afip.environment || :production
7
+ url = Afip::AuthData.environment == :production ? "aws" : "awshomo"
8
+ Afip.service_url = "https://#{url}.afip.gov.ar/sr-padron/webservices/personaServiceA4?WSDL"
9
+ Afip.cuit = "20368642682"
10
+ Afip.cert = "#{Rails.root}/afip/desideral_prod.crt"
11
+ Afip.pkey = "#{Rails.root}/afip/desideral.key"
12
+ Afip::AuthData.fetch("ws_sr_padron_a4")
13
+
14
+ @client = Savon.client(
15
+ ssl_cert_key_file: "#{Rails.root}/afip/desideral.key",
16
+ ssl_cert_file: "#{Rails.root}/afip/desideral_prod.crt",
17
+ env_namespace: :soapenv,
18
+ namespace_identifier: :a4,
19
+ encoding: 'UTF-8',
20
+ wsdl: Afip.service_url
21
+ )
22
+
23
+ @dni = attrs[:dni].rjust(8, "0")
24
+ @tipo = attrs[:tipo] || "F" #F femenino M masculino E empresa
25
+ @cuit = get_cuit
26
+ end
27
+
28
+ def get_persona
29
+ body = setup_body
30
+
31
+ pp response = client.call(:get_persona,message: body)
32
+ rescue Savon::SOAPFault => error
33
+ if !error.blank?
34
+ pp @fault_code = error.to_hash[:fault][:faultstring]
35
+ end
36
+ return response
37
+ end
38
+
39
+ def get_data
40
+ @data = get_persona
41
+ if not fault_code
42
+ set_data
43
+ else
44
+ return nil
45
+ end
46
+
47
+ end
48
+
49
+ def set_data
50
+ pp data.body
51
+ if not data.body[:get_persona_response][:persona_return][:persona][:actividad].nil?
52
+ {
53
+ :last_name => data.body[:get_persona_response][:persona_return][:persona][:apellido],
54
+ :first_name => data.body[:get_persona_response][:persona_return][:persona][:nombre],
55
+ :cuit => data.body[:get_persona_response][:persona_return][:persona][:id_persona],
56
+ :cp => data.body[:get_persona_response][:persona_return][:persona][:domicilio].last[:cod_postal],
57
+ :address => data.body[:get_persona_response][:persona_return][:persona][:domicilio].last[:direccion],
58
+ :city_id => data.body[:get_persona_response][:persona_return][:persona][:domicilio].last[:id_provincia],
59
+ :city => PROVINCIAS[data.body[:get_persona_response][:persona_return][:persona][:domicilio].last[:id_provincia]],
60
+ :locality => data.body[:get_persona_response][:persona_return][:persona][:domicilio].last[:localidad],
61
+ :birthday => data.body[:get_persona_response][:persona_return][:persona][:fecha_nacimiento].to_date
62
+ }
63
+ else
64
+ {
65
+ :last_name => Padron.divide_name(data.body[:get_persona_response][:persona_return][:persona][:apellido])[0],
66
+ :first_name => Padron.divide_name(data.body[:get_persona_response][:persona_return][:persona][:apellido])[1],
67
+ :cuit => data.body[:get_persona_response][:persona_return][:persona][:id_persona],
68
+ :cp => data.body[:get_persona_response][:persona_return][:persona][:domicilio][:cod_postal],
69
+ :address => data.body[:get_persona_response][:persona_return][:persona][:domicilio][:direccion],
70
+ :city_id => data.body[:get_persona_response][:persona_return][:persona][:domicilio][:id_provincia],
71
+ :city => PROVINCIAS[data.body[:get_persona_response][:persona_return][:persona][:domicilio][:id_provincia]],
72
+ :locality => data.body[:get_persona_response][:persona_return][:persona][:domicilio][:localidad],
73
+ :birthday => data.body[:get_persona_response][:persona_return][:persona][:fecha_nacimiento].to_date
74
+ }
75
+ end
76
+ end
77
+
78
+ def self.divide_name(full_name)
79
+ full_name = full_name.strip.split(/\s+/)
80
+ last_name = ''
81
+ last = (full_name.count / 2) - 1
82
+ (0..last).each do |i|
83
+ if i != last
84
+ last_name += full_name[i] + ' '
85
+ else
86
+ last_name += full_name[i]
87
+ end
88
+ end
89
+ full_name = full_name - (last_name.strip.split(/\s+/))
90
+ first_name = full_name.join(", ").gsub(",","").split.map(&:capitalize).join(' ')
91
+ last_name = last_name.split.map(&:capitalize).join(' ')
92
+ return [last_name, first_name]
93
+ end
94
+
95
+ def get_cuit
96
+ if dni.length == 11
97
+ @cuit = @dni
98
+ else
99
+ @cuit = calculate_cuit
100
+ end
101
+ end
102
+
103
+ def calculate_cuit
104
+ multiplicador = "2345672345"
105
+
106
+ case tipo
107
+ when "F"
108
+ xy = 27
109
+ xy_dni = "27#{dni}"
110
+ when "M"
111
+ xy = 20
112
+ xy_dni = "20#{dni}"
113
+ end
114
+ verificador = 0
115
+ (0..9).each do |i|
116
+ verificador += (xy_dni.reverse[i].to_i * multiplicador[i].to_i)
117
+ end
118
+ pp verificador
119
+ z = verificador - (verificador / 11 * 11)
120
+
121
+ case z
122
+ when 0
123
+ z = 0
124
+ when 1
125
+ if tipo == "M"
126
+ z = 9
127
+ xy = 23
128
+ elsif tipo == "F"
129
+ z = 4
130
+ xy = 23
131
+ else
132
+ z = 11 - z
133
+ end
134
+ else
135
+ z = 11 - z
136
+ end
137
+
138
+ return "#{xy}#{dni}#{z}"
139
+ end
140
+
141
+ def setup_body
142
+ body = {
143
+ 'token' => Afip::TOKEN,
144
+ 'sign' => Afip::SIGN,
145
+ 'cuitRepresentada' => Afip.cuit,
146
+ 'idPersona' => @cuit.to_s
147
+ }
148
+ end
149
+
150
+ PROVINCIAS = {
151
+ "0" => 'CIUDAD AUTONOMA BUENOS AIRES',
152
+ "1" => 'BUENOS AIRES',
153
+ "2" => 'CATAMARCA',
154
+ "3" => 'CORDOBA',
155
+ "4" => 'CORRIENTES',
156
+ "5" => 'ENTRE RIOS',
157
+ "6" => 'JUJUY',
158
+ "7" => 'MENDOZA',
159
+ "8" => 'LA RIOJA',
160
+ "9" => 'SALTA',
161
+ "10" => 'SAN JUAN',
162
+ "11" => 'SAN LUIS',
163
+ "12" => 'SANTA FE',
164
+ "13" => 'SANTIAGO DEL ESTERO',
165
+ "14" => 'TUCUMAN',
166
+ "16" => 'CHACO',
167
+ "17" => 'CHUBUT',
168
+ "18" => 'FORMOSA',
169
+ "19" => 'MISIONES',
170
+ "20" => 'NEUQUEN',
171
+ "21" => 'LA PAMPA',
172
+ "22" => 'RIO NEGRO',
173
+ "23" => 'SANTA CRUZ',
174
+ "24" => 'TIERRA DEL FUEGO'
175
+ }
176
+ end
177
+ end
178
+
179
+
180
+
181
+
data/lib/Afip/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Afip
2
- VERSION = "0.1.1"
2
+ VERSION = "0.1.2"
3
3
  end
data/lib/Afip/wsaa.rb ADDED
@@ -0,0 +1,94 @@
1
+ module Afip
2
+ # Authorization class. Handles interactions wiht the WSAA, to provide
3
+ # valid key and signature that will last for a day.
4
+ #
5
+ class Wsaa
6
+ # Main method for authentication and authorization.
7
+ # When successful, produces the yaml file with auth data.
8
+ #
9
+ def self.login(service = "wsfe")
10
+ tra = build_tra(service)
11
+ cms = build_cms(tra)
12
+ req = build_request(cms)
13
+ auth = call_wsaa(req)
14
+
15
+ write_yaml(auth)
16
+ end
17
+
18
+ protected
19
+ # Builds the xml for the 'Ticket de Requerimiento de Acceso'
20
+ # @return [String] containing the request body
21
+ #
22
+ def self.build_tra service
23
+ @now = (Time.now) - 120
24
+ @from = @now.strftime('%FT%T%:z')
25
+ @to = (@now + ((12*60*60))).strftime('%FT%T%:z')
26
+ @id = @now.strftime('%s')
27
+ tra = <<-EOF
28
+ <?xml version="1.0" encoding="UTF-8"?>
29
+ <loginTicketRequest version="1.0">
30
+ <header>
31
+ <uniqueId>#{ @id }</uniqueId>
32
+ <generationTime>#{ @from }</generationTime>
33
+ <expirationTime>#{ @to }</expirationTime>
34
+ </header>
35
+ <service>#{service}</service>
36
+ </loginTicketRequest>
37
+ EOF
38
+ return tra
39
+ end
40
+
41
+ # Builds the CMS
42
+ # @return [String] cms
43
+ #
44
+ def self.build_cms(tra)
45
+ cms = `echo '#{ tra }' |
46
+ #{ Afip.openssl_bin } cms -sign -in /dev/stdin -signer #{ Afip.cert } -inkey #{ Afip.pkey } -nodetach \
47
+ -outform der |
48
+ #{ Afip.openssl_bin } base64 -e`
49
+ return cms
50
+ end
51
+
52
+ # Builds the CMS request to log in to the server
53
+ # @return [String] the cms body
54
+ #
55
+ def self.build_request(cms)
56
+ request = <<-XML
57
+ <?xml version="1.0" encoding="UTF-8"?>
58
+ <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://wsaa.view.sua.dvadac.desein.afip.gov">
59
+ <SOAP-ENV:Body>
60
+ <ns1:loginCms>
61
+ <ns1:in0>
62
+ #{ cms }
63
+ </ns1:in0>
64
+ </ns1:loginCms>
65
+ </SOAP-ENV:Body>
66
+ </SOAP-ENV:Envelope>
67
+ XML
68
+ return request
69
+ end
70
+
71
+ # Calls the WSAA with the request built by build_request
72
+ # @return [Array] with the token and signature
73
+ #
74
+ def self.call_wsaa(req)
75
+ response = `echo '#{ req }' | curl -k -s -H 'Content-Type: application/soap+xml; action=""' -d @- #{ Afip::AuthData.wsaa_url }`
76
+ pp response
77
+ response = CGI::unescapeHTML(response)
78
+ token = response.scan(/\<token\>(.+)\<\/token\>/).first.first
79
+ sign = response.scan(/\<sign\>(.+)\<\/sign\>/).first.first
80
+ return [token, sign]
81
+ end
82
+
83
+ # Writes the token and signature to a YAML file in the /tmp directory
84
+ #
85
+ def self.write_yaml(certs)
86
+ yml = <<-YML
87
+ token: #{certs[0]}
88
+ sign: #{certs[1]}
89
+ YML
90
+ `echo '#{ yml }' > /tmp/#{Afip::AuthData.environment.to_s}_Afip_#{ Afip.cuit }_#{ Time.new.strftime('%Y_%m_%d') }.yml`
91
+ end
92
+
93
+ end
94
+ end
data/lib/Afip.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  require "Afip/version"
2
2
  require "bundler/setup"
3
- require "Afip/constants"
3
+ require "./Afip/constants"
4
4
  require "savon"
5
5
  require "Afip/core_ext/float"
6
6
  require "Afip/core_ext/hash"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: Afip
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Facundo A. Díaz Martínez
@@ -45,16 +45,29 @@ executables: []
45
45
  extensions: []
46
46
  extra_rdoc_files: []
47
47
  files:
48
+ - ".DS_Store"
48
49
  - ".gitignore"
50
+ - Afip-0.1.0.gem
49
51
  - Afip.gemspec
50
52
  - Gemfile
53
+ - Gemfile.lock
51
54
  - LICENSE.txt
52
55
  - README.md
53
56
  - Rakefile
54
57
  - bin/console
55
58
  - bin/setup
59
+ - lib/.DS_Store
56
60
  - lib/Afip.rb
61
+ - lib/Afip/auth_data.rb
62
+ - lib/Afip/authorizer.rb
63
+ - lib/Afip/bill.rb
64
+ - lib/Afip/constants.rb
65
+ - lib/Afip/core_ext/float.rb
66
+ - lib/Afip/core_ext/hash.rb
67
+ - lib/Afip/core_ext/string.rb
68
+ - lib/Afip/padron.rb
57
69
  - lib/Afip/version.rb
70
+ - lib/Afip/wsaa.rb
58
71
  homepage: https://www.desideral.com
59
72
  licenses:
60
73
  - MIT