afip_wsfe 0.1.3 → 0.2.0

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
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5d8c89ca3694469f3c4c373a59f0211581eb1ce4
4
- data.tar.gz: 95516b46686e0106d211d6614527e05e3ac857b5
3
+ metadata.gz: dc15950903b8df7c10b8e916138819dcbc297011
4
+ data.tar.gz: a7ddc21e6172a123c72db29db85f60d11be174f8
5
5
  SHA512:
6
- metadata.gz: bc8aefda1c084f7cf580b32feb10969c32637035af58ac33564623ecb70ea6aa533aaf7a894462c8088a8ed07fc5251e9c5fec5ee1eba5d4194adbb5e91b8137
7
- data.tar.gz: 90ee766a5f0077d26b4af8917c22c37632abbfad88065cd8a5a55958ee5ae1f9a13559173fba638e027982fd0cbe7a36f881afa8d7acbc7ba8c126075e1129d7
6
+ metadata.gz: 28bac366586ce202a93d2bca0e06b46131305ac7256b2ba3749371c791e86dcd029014968ec2e4221a1c0bf0472fefa1da0b9fc59df65eaf36d295fbdc92216e
7
+ data.tar.gz: 24210c6f314c98e6faa46c7f921c92b5f6a49dc21f24bb7d4315df0c1e85a553189d9798e5315cec7a5d34dbbd5d723ad9435369f8defd2a1f66af3195a55703
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.3
1
+ 0.2.0
data/afip_wsfe.gemspec CHANGED
@@ -2,16 +2,16 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Juwelier::Tasks in rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
- # stub: afip_wsfe 0.1.3 ruby lib
5
+ # stub: afip_wsfe 0.2.0 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "afip_wsfe".freeze
9
- s.version = "0.1.3"
9
+ s.version = "0.2.0"
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
12
12
  s.require_paths = ["lib".freeze]
13
13
  s.authors = ["Paco Moreno".freeze]
14
- s.date = "2018-07-02"
14
+ s.date = "2018-07-04"
15
15
  s.description = "Wrapper para usar web service de factura electr\u{f3}nica de AFIP".freeze
16
16
  s.email = "pakerimus@gmail.com".freeze
17
17
  s.extra_rdoc_files = [
@@ -31,8 +31,8 @@ Gem::Specification.new do |s|
31
31
  "afip_wsfe.gemspec",
32
32
  "lib/afip_wsfe.rb",
33
33
  "lib/afip_wsfe/auth_data.rb",
34
- "lib/afip_wsfe/authorizer.rb",
35
34
  "lib/afip_wsfe/bill.rb",
35
+ "lib/afip_wsfe/client.rb",
36
36
  "lib/afip_wsfe/constants.rb",
37
37
  "lib/afip_wsfe/version.rb",
38
38
  "lib/afip_wsfe/wsaa.rb",
@@ -6,26 +6,33 @@ module AfipWsfe
6
6
 
7
7
  attr_accessor :todays_data_file_name
8
8
 
9
+ def auth_hash
10
+ fetch unless AfipWsfe.constants.include?(:TOKEN) && AfipWsfe.constants.include?(:SIGN)
11
+
12
+ {
13
+ auth: {
14
+ token: AfipWsfe::TOKEN,
15
+ sign: AfipWsfe::SIGN,
16
+ cuit: AfipWsfe.cuit,
17
+ }
18
+ }
19
+ end
20
+
21
+ def todays_data_file_name
22
+ @todays_data_file ||= "/tmp/afip_wsfe_#{ AfipWsfe.cuit }_#{ Time.zone.today.strftime('%Y_%m_%d') }.yml"
23
+ end
24
+
25
+ private
26
+
9
27
  def fetch
10
- todays_data_file_exists = if File.exists?(todays_data_file_name)
11
- true
12
- else
28
+ unless File.exists?(todays_data_file_name)
13
29
  wsaa = AfipWsfe::Wsaa.new
14
30
  wsaa.login
15
31
  end
16
32
 
17
33
  YAML.load_file(todays_data_file_name).each do |k, v|
18
- AfipWsfe.const_set(k.to_s.upcase, v) unless AfipWsfe.const_defined?(k.to_s.upcase)
19
- end if todays_data_file_exists
20
- end
21
-
22
- def auth_hash
23
- fetch unless AfipWsfe.constants.include?(:TOKEN) && AfipWsfe.constants.include?(:SIGN)
24
- { 'Token' => AfipWsfe::TOKEN, 'Sign' => AfipWsfe::SIGN, 'Cuit' => AfipWsfe.cuit }
25
- end
26
-
27
- def todays_data_file_name
28
- @todays_data_file ||= "/tmp/bravo_#{ AfipWsfe.cuit }_#{ Time.zone.today.strftime('%Y_%m_%d') }.yml"
34
+ AfipWsfe.const_set(k.to_s.upcase, v)
35
+ end
29
36
  end
30
37
 
31
38
  def remove
@@ -1,39 +1,22 @@
1
1
  module AfipWsfe
2
2
  class Bill
3
- attr_reader :client, :base_imp, :total
4
- attr_accessor :net, :doc_num, :iva_cond, :documento, :concepto, :moneda,
5
- :due_date, :fch_serv_desde, :fch_serv_hasta, :fch_emision,
6
- :body, :response, :ivas, :nro_comprobante
3
+ attr_reader :base_imp, :total
4
+ attr_accessor :fch_emision, :fch_vto_pago, :fch_serv_desde, :fch_serv_hasta,
5
+ :nro_comprobante, :doc_num, :net,
6
+ :iva_cond, :documento, :concepto, :moneda, :ivas,
7
+ :body, :response
7
8
 
8
9
  def initialize(attrs = {})
9
- AfipWsfe.environment ||= :test
10
- AfipWsfe::AuthData.fetch
11
-
12
- @client = Savon.client(
13
- wsdl: AfipWsfe::URLS[AfipWsfe.environment][:wsfe],
14
- namespaces: {
15
- "xmlns:soapenv" => "http://schemas.xmlsoap.org/soap/envelope/",
16
- "xmlns:ar" => "http://ar.gov.afip.dif.FEV1/"
17
- },
18
- log: AfipWsfe.log?,
19
- log_level: AfipWsfe.log_level || :debug,
20
- ssl_cert_key_file: AfipWsfe.pkey,
21
- ssl_cert_file: AfipWsfe.cert,
22
- ssl_verify_mode: :none,
23
- read_timeout: 90,
24
- open_timeout: 90,
25
- headers: {
26
- "Accept-Encoding" => "gzip, deflate",
27
- "Connection" => "Keep-Alive"
28
- }
29
- )
10
+ @client = AfipWsfe::Client.new
11
+ @endpoint = :wsfe
12
+ @response = nil
13
+ @status = false
30
14
 
31
- @body = {"Auth" => AfipWsfe.auth_hash}
32
- @net = attrs[:net] || 0
33
- self.documento = attrs[:documento] || AfipWsfe.default_documento
34
- self.moneda = attrs[:moneda] || AfipWsfe.default_moneda
15
+ self.net = attrs[:net] || 0
35
16
  self.iva_cond = attrs[:iva_cond] || :responsable_monotributo
17
+ self.documento = attrs[:documento] || AfipWsfe.default_documento
36
18
  self.concepto = attrs[:concepto] || AfipWsfe.default_concepto
19
+ self.moneda = attrs[:moneda] || AfipWsfe.default_moneda
37
20
  self.ivas = attrs[:ivas] || Array.new # [ 1, 100.00, 10.50 ], [ 2, 100.00, 21.00 ]
38
21
  end
39
22
 
@@ -56,10 +39,8 @@ module AfipWsfe
56
39
 
57
40
  def exchange_rate
58
41
  return 1 if moneda == :peso
59
- savon_response = client.call :fe_param_get_cotizacion do
60
- body.merge!({"MonId" => AfipWsfe::MONEDAS[moneda][:codigo]})
61
- end
62
- savon_response.to_hash[:fe_param_get_cotizacion_response][:fe_param_get_cotizacion_result][:result_get][:mon_cotiz].to_f
42
+ @status, @response = @client.call_endpoint @endpoint, :fe_param_get_cotizacion, {"MonId" => AfipWsfe::MONEDAS[moneda][:codigo]}
43
+ @response[:result_get][:mon_cotiz].to_f
63
44
  end
64
45
 
65
46
  def total
@@ -75,16 +56,34 @@ module AfipWsfe
75
56
  end
76
57
 
77
58
  def authorize
78
- body = setup_bill
79
- savon_response = client.call(:fecae_solicitar, message: body)
80
- setup_response(savon_response.to_hash)
59
+ setup_bill
60
+ @status, @response = @client.call_endpoint(@endpoint, :fecae_solicitar, self.body)
61
+ setup_response
81
62
  self.authorized?
82
63
  end
83
64
 
65
+ def last_bill_number
66
+ params = {"PtoVta" => AfipWsfe.sale_point, "CbteTipo" => cbte_type}
67
+ @status, @response = @client.call_endpoint @endpoint, :fe_comp_ultimo_autorizado, params
68
+ @response[:cbte_nro].to_i
69
+ end
70
+
71
+ def next_bill_number
72
+ last_bill_number + 1
73
+ end
74
+
75
+ def authorized?
76
+ @response && @response[:header_result] == "A"
77
+ end
78
+
79
+ private
80
+
84
81
  def setup_bill
85
- fecha_emision = (fch_emision || Time.zone.today).strftime('%Y%m%d')
82
+ today = Time.zone.today.strftime('%Y%m%d')
83
+
84
+ fecha_emision = (fch_emision || today)
86
85
 
87
- nro_comprobante ||= next_bill_number
86
+ self.nro_comprobante ||= next_bill_number
88
87
 
89
88
  array_ivas = Array.new
90
89
  self.ivas.each{ |i|
@@ -96,7 +95,11 @@ module AfipWsfe
96
95
 
97
96
  fecaereq = {
98
97
  "FeCAEReq" => {
99
- "FeCabReq" => AfipWsfe::Bill.header(cbte_type),
98
+ "FeCabReq" => {
99
+ "CantReg" => "1",
100
+ "CbteTipo" => cbte_type,
101
+ "PtoVta" => AfipWsfe.sale_point
102
+ },
100
103
  "FeDetReq" => {
101
104
  "FECAEDetRequest" => {
102
105
  "CbteDesde" => nro_comprobante,
@@ -119,7 +122,7 @@ module AfipWsfe
119
122
  detail = fecaereq["FeCAEReq"]["FeDetReq"]["FECAEDetRequest"]
120
123
 
121
124
  if AfipWsfe.own_iva_cond == :responsable_monotributo
122
- detail["ImpTotal"] = net.to_f
125
+ detail["ImpTotal"] = net.to_f.round(2)
123
126
  else
124
127
  detail["ImpIVA"] = iva_sum
125
128
  detail["ImpTotal"] = total.to_f.round(2)
@@ -129,52 +132,30 @@ module AfipWsfe
129
132
  unless concepto == "Productos" # En "Productos" ("01"), si se mandan estos parámetros la afip rechaza.
130
133
  detail.merge!({"FchServDesde" => fch_serv_desde || today,
131
134
  "FchServHasta" => fch_serv_hasta || today,
132
- "FchVtoPago" => due_date || today})
133
- end
134
-
135
- body.merge!(fecaereq)
136
- end
137
-
138
- def next_bill_number
139
- var = {"Auth" => AfipWsfe.auth_hash,"PtoVta" => AfipWsfe.sale_point, "CbteTipo" => cbte_type}
140
- resp = client.call :fe_comp_ultimo_autorizado do
141
- message(var)
135
+ "FchVtoPago" => fch_vto_pago || today})
142
136
  end
143
137
 
144
- resp.to_hash[:fe_comp_ultimo_autorizado_response][:fe_comp_ultimo_autorizado_result][:cbte_nro].to_i + 1
145
- end
146
-
147
- def authorized?
148
- !response.nil? && response[:header_result] == "A" && response[:detail_result] == "A"
149
- end
150
-
151
- private
152
-
153
- class << self
154
- def header(cbte_type)#todo sacado de la factura
155
- {"CantReg" => "1", "CbteTipo" => cbte_type, "PtoVta" => AfipWsfe.sale_point}
156
- end
138
+ self.body = fecaereq
157
139
  end
158
140
 
159
- def setup_response(the_response)
160
- result = the_response[:fecae_solicitar_response][:fecae_solicitar_result]
161
-
162
- if not result[:fe_det_resp] or not result[:fe_cab_resp] then
163
- self.response = {
164
- errores: result[:errors],
141
+ def setup_response
142
+ if not @response[:fe_det_resp] or not @response[:fe_cab_resp]
143
+ @response = {
144
+ errores: @response[:errors],
165
145
  header_result: {resultado: "X"},
146
+ detail_result: {resultado: "X"},
166
147
  observaciones: nil
167
148
  }
168
149
  return
169
150
  end
170
151
 
171
- response_header = result[:fe_cab_resp]
172
- response_detail = result[:fe_det_resp][:fecae_det_response]
152
+ response_header = @response[:fe_cab_resp]
153
+ response_detail = @response[:fe_det_resp][:fecae_det_response]
173
154
 
174
- request_header = body["FeCAEReq"]["FeCabReq"].underscore_keys.symbolize_keys
175
- request_detail = body["FeCAEReq"]["FeDetReq"]["FECAEDetRequest"].underscore_keys.symbolize_keys
155
+ request_header = body["FeCAEReq"]["FeCabReq"].transform_keys { |key| key.to_s.downcase.to_sym }
156
+ request_detail = body["FeCAEReq"]["FeDetReq"]["FECAEDetRequest"].transform_keys { |key| key.to_s.downcase.to_sym }
176
157
 
177
- response_detail.merge!( result[:errors] ) if result[:errors]
158
+ response_detail.merge!( @response[:errors] ) if @response[:errors]
178
159
 
179
160
  self.response = {
180
161
  header_result: response_header.delete(:resultado),
@@ -0,0 +1,24 @@
1
+ module AfipWsfe
2
+ class Client
3
+ def initialize(authenticate=true)
4
+ AfipWsfe.environment ||= :test
5
+ @auth = authenticate ? AfipWsfe.auth_hash : {}
6
+ end
7
+
8
+ def call_endpoint(endpoint, savon_method, params={})
9
+ return_key = endpoint == :wsaa ? :"#{savon_method}_return" : :"#{savon_method}_result"
10
+
11
+ result = Savon.client(
12
+ log: AfipWsfe.log?,
13
+ log_level: AfipWsfe.log_level || :debug,
14
+ wsdl: "#{AfipWsfe::URLS[AfipWsfe.environment][endpoint]}?wsdl",
15
+ convert_request_keys_to: :camelcase
16
+ ).call(savon_method, message: params.merge(@auth))
17
+
18
+ response = result.body[:"#{savon_method}_response"][return_key]
19
+ Hash.from_xml response if endpoint == :wsaa
20
+
21
+ [result.success?, response]
22
+ end
23
+ end
24
+ end
@@ -88,11 +88,11 @@ module AfipWsfe
88
88
  # This hash keeps the set of urls for wsaa and wsfe for production and testing envs
89
89
  URLS = {
90
90
  test: {
91
- wsaa: 'https://wsaahomo.afip.gov.ar/ws/services/LoginCms?wsdl',
92
- wsfe: 'https://wswhomo.afip.gov.ar/wsfev1/service.asmx?WSDL'
91
+ wsaa: 'https://wsaahomo.afip.gov.ar/ws/services/LoginCms',
92
+ wsfe: 'https://wswhomo.afip.gov.ar/wsfev1/service.asmx'
93
93
  },
94
94
  production: {
95
- wsaa: 'https://wsaa.afip.gov.ar/ws/services/LoginCms?wsdl',
95
+ wsaa: 'https://wsaa.afip.gov.ar/ws/services/LoginCms',
96
96
  wsfe: 'https://servicios1.afip.gov.ar/wsfev1/service.asmx'
97
97
  }
98
98
  }
@@ -1,4 +1,4 @@
1
1
  module AfipWsfe
2
2
  # Gem version
3
- VERSION = '0.1.0'
3
+ VERSION = '0.2.0'
4
4
  end
@@ -5,45 +5,23 @@ module AfipWsfe
5
5
  class Wsaa
6
6
 
7
7
  def initialize(url=nil)
8
- AfipWsfe.environment ||= :test
9
- @client = Savon.client(
10
- wsdl: AfipWsfe::URLS[AfipWsfe.environment][:wsaa],
11
- namespaces: {
12
- "xmlns:soapenv" => "http://schemas.xmlsoap.org/soap/envelope/",
13
- "xmlns:ar" => "http://ar.gov.afip.dif.FEV1/"
14
- },
15
- log: AfipWsfe.log?,
16
- log_level: AfipWsfe.log_level || :debug,
17
- ssl_verify_mode: :none,
18
- read_timeout: 90,
19
- open_timeout: 90,
20
- headers: {
21
- "Accept-Encoding" => "gzip, deflate",
22
- "Connection" => "Keep-Alive"
23
- },
24
- convert_request_keys_to: :none
25
- )
26
-
27
- @cms = nil
28
- @performed = false
8
+ @client = AfipWsfe::Client.new(false)
9
+ @endpoint = :wsaa
29
10
  @response = nil
11
+ @status = false
30
12
  end
31
13
 
32
14
  def login
15
+ raise "Ruta del archivo de llave privada no declarado" unless AfipWsfe.pkey.present?
16
+ raise "Ruta del archivo certificado no declarado" unless AfipWsfe.cert.present?
33
17
  raise "Archivo de llave privada no encontrado en #{ AfipWsfe.pkey }" unless File.exists?(AfipWsfe.pkey)
34
18
  raise "Archivo certificado no encontrado en #{ AfipWsfe.cert }" unless File.exists?(AfipWsfe.cert)
35
- build_tra
36
- call_web_service
19
+ @status, @response = @client.call_endpoint @endpoint, :login_cms, {in0: build_tra}
37
20
  parse_response
38
- status
21
+ @status
39
22
  end
40
23
 
41
- def status
42
- return false unless @performed
43
- @response.success?
44
- end
45
-
46
- protected
24
+ private
47
25
 
48
26
  def build_tra
49
27
  now = Time.zone.now
@@ -60,34 +38,31 @@ module AfipWsfe
60
38
  "service" => "wsfe"
61
39
  }.to_xml(root: "loginTicketRequest")
62
40
 
63
- @cms = `echo '#{ tra }' |
64
- #{ AfipWsfe.openssl_bin } cms -sign -in /dev/stdin -signer #{ AfipWsfe.cert } -inkey #{ AfipWsfe.pkey } -nodetach -outform der |
65
- #{ AfipWsfe.openssl_bin } base64 -e`
66
- end
67
-
68
- def call_web_service
69
- @response = @client.call :login_cms, message: {in0: @cms}
70
- @performed = true
41
+ pkcs7 = OpenSSL::PKCS7.sign(cert, key, tra)
42
+ OpenSSL::PKCS7.write_smime(pkcs7).lines[5..-2].join()
71
43
  end
72
44
 
73
45
  def parse_response
74
- if status
75
- response_hash = Hash.from_xml @response.body[:login_cms_response][:login_cms_return]
76
- token = response_hash["loginTicketResponse"]["credentials"]["token"]
77
- sign = response_hash["loginTicketResponse"]["credentials"]["sign"]
78
- write_yaml(token, sign)
79
- end
46
+ write_yaml(@response["loginTicketResponse"]["credentials"])
80
47
  end
81
48
 
82
- def write_yaml(token, sign)
83
- filename = "/tmp/bravo_#{ AfipWsfe.cuit }_#{ Time.zone.today.strftime('%Y_%m_%d') }.yml"
49
+ def write_yaml(credentials)
50
+ filename = AuthData.todays_data_file_name
84
51
  content = {
85
- token: token,
86
- sign: sign
52
+ token: credentials["token"],
53
+ sign: credentials["sign"]
87
54
  }
88
55
  File.open(filename, 'w') { |f|
89
56
  f.write content.to_yaml
90
57
  }
91
58
  end
59
+
60
+ def cert
61
+ OpenSSL::X509::Certificate.new(File.read(AfipWsfe.cert))
62
+ end
63
+
64
+ def key
65
+ OpenSSL::PKey::RSA.new(File.read(AfipWsfe.pkey))
66
+ end
92
67
  end
93
68
  end
data/lib/afip_wsfe.rb CHANGED
@@ -1,8 +1,9 @@
1
1
  # encoding: utf-8
2
2
  require 'bundler/setup'
3
+ require 'savon'
3
4
  require 'afip_wsfe/version'
4
5
  require 'afip_wsfe/constants'
5
- require 'savon'
6
+ require 'afip_wsfe/client'
6
7
 
7
8
  require 'net/http'
8
9
  require 'net/https'
@@ -12,21 +13,21 @@ module AfipWsfe
12
13
  # Exception Class for missing or invalid attributes
13
14
  class NullOrInvalidAttribute < StandardError; end
14
15
 
15
- autoload :Constants, 'afip_wsfe/constants'
16
- autoload :Authorizer, 'afip_wsfe/authorizer'
17
- autoload :AuthData, 'afip_wsfe/auth_data'
18
- autoload :Bill, 'afip_wsfe/bill'
19
- autoload :Wsaa, 'afip_wsfe/wsaa'
16
+ autoload :Constants, 'afip_wsfe/constants'
17
+ autoload :AuthData, 'afip_wsfe/auth_data'
18
+ autoload :Client, 'afip_wsfe/client'
19
+ autoload :Wsaa, 'afip_wsfe/wsaa'
20
+ autoload :Bill, 'afip_wsfe/bill'
20
21
 
21
22
  extend self
22
23
 
23
24
  attr_accessor :environment, :verbose, :log_level,
24
- :pkey, :cert, :openssl_bin,
25
+ :pkey, :cert,
25
26
  :cuit, :own_iva_cond, :sale_point,
26
27
  :default_documento, :default_concepto, :default_moneda
27
28
 
28
29
  def auth_hash
29
- {"Token" => AfipWsfe::TOKEN, "Sign" => AfipWsfe::SIGN, "Cuit" => AfipWsfe.cuit}
30
+ AuthData.auth_hash
30
31
  end
31
32
 
32
33
  def log?
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: afip_wsfe
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paco Moreno
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-07-02 00:00:00.000000000 Z
11
+ date: 2018-07-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: shoulda
@@ -114,8 +114,8 @@ files:
114
114
  - afip_wsfe.gemspec
115
115
  - lib/afip_wsfe.rb
116
116
  - lib/afip_wsfe/auth_data.rb
117
- - lib/afip_wsfe/authorizer.rb
118
117
  - lib/afip_wsfe/bill.rb
118
+ - lib/afip_wsfe/client.rb
119
119
  - lib/afip_wsfe/constants.rb
120
120
  - lib/afip_wsfe/version.rb
121
121
  - lib/afip_wsfe/wsaa.rb
@@ -1,10 +0,0 @@
1
- module AfipWsfe
2
- class Authorizer
3
- attr_reader :pkey, :cert
4
-
5
- def initialize
6
- @pkey = AfipWsfe.pkey
7
- @cert = AfipWsfe.cert
8
- end
9
- end
10
- end