afip_wsfe 0.1.3 → 0.2.0

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