diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..e67404f --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,163 @@ +--- +AllCops: + Exclude: + - 'doc/' + - 'coverage/' + - 'pkg/' + - 'tmp/' + +Metrics/CyclomaticComplexity: + Severity: error + Max: 8 + +Metrics/LineLength: + Max: 110 + Severity: error + +Metrics/ClassLength: + Max: 150 + Severity: error + +Metrics/MethodLength: + Max: 15 + Severity: error + +Metrics/ParameterLists: + Max: 5 + Severity: error + +Metrics/PerceivedComplexity: + Max: 10 + Severity: error + +Lint/EndAlignment: + AlignWith: variable + +Lint/UselessAssignment: + Severity: error + +Lint/ShadowingOuterLocalVariable: + Severity: convention + +Style/CaseEquality: + Enabled: false + +Style/Documentation: + Enabled: false + Severity: error + +Style/IfUnlessModifier: + MaxLineLength: 80 + +Style/GuardClause: + MinBodyLength: 3 + +Style/Lambda: + Enabled: false + +Style/EmptyLinesAroundBody: + Enabled: false + +Style/EmptyLineBetweenDefs: + AllowAdjacentOneLineDefs: true + +Style/ClassAndModuleChildren: + Enabled: false + +Style/AndOr: + EnforcedStyle: conditionals + +# This one is usefull for class attr_* +Style/TrivialAccessors: + AllowDSLWriters: true + +Style/AlignParameters: + EnforcedStyle: with_fixed_indentation + +Style/AlignHash: + EnforcedHashRocketStyle: key + EnforcedLastArgumentHashStyle: always_ignore + +Style/ModuleFunction: + Enabled: false + +Style/RegexpLiteral: + # The maximum number of (escaped) slashes that a slash-delimited regexp is + # allowed to have. If there are more slashes, a %r regexp shall be used. + MaxSlashes: 0 + +Style/FormatString: + Enabled: false + +Style/SingleLineBlockParams: + Enabled: false + +Style/CaseIndentation: + IndentWhenRelativeTo: end + +Style/PredicateName: + Severity: error + +Style/IndentHash: + EnforcedStyle: consistent + +Style/MultilineBlockChain: + Severity: error + +Lint/AssignmentInCondition: + Enabled: false + +Style/Alias: + Enabled: false + +Style/StringLiterals: + Enabled: false + +Style/SpaceInsideBlockBraces: + SpaceBeforeBlockParameters: true + +Style/PercentLiteralDelimiters: + PreferredDelimiters: + '%i': '[]' + '%w': '[]' + '%W': '[]' + '%': '{}' + +Style/CollectionMethods: + PreferredMethods: + reduce: inject + collect: map + collect!: 'map!' + detect: find + detect!: 'find!' + find_all: select + find_all!: 'select!' + +Style/DoubleNegation: + Severity: error + +Style/SignalException: + EnforcedStyle: only_raise + +Style/DotPosition: + EnforcedStyle: trailing + +Style/SingleLineMethods: + AllowIfMethodIsEmpty: true + +# It doesnt handle cases when we want to align multiple methods call into a table like +Style/SingleSpaceBeforeFirstArg: + Enabled: false + +# replaces $1 x Regexp.last_match[1] +Style/PerlBackrefs: + Enabled: false + +# There are valid cases (eg. Date.parse(date) rescue nil) +Style/RescueModifier: + Enabled: false + +Style/Blocks: + Severity: error + Exclude: + - '**/spec/**/*_spec.rb' diff --git a/.travis.yml b/.travis.yml index 8c01670..e810ca2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,11 @@ language: ruby rvm: - - 1.9.3 - 2.0.0 + - 2.1.2 bundler_args: --without test branches: only: - master +script: + - bundle exec rspec spec + - bundle exec rubocop diff --git a/Gemfile b/Gemfile index 8021c6c..3061272 100644 --- a/Gemfile +++ b/Gemfile @@ -3,7 +3,6 @@ source 'http://rubygems.org' group :test do gem 'guard-rspec', '~> 2.4.0' gem 'rb-fsevent', '~> 0.9.1' - gem 'debugger', '~> 1.3.0' end -gemspec \ No newline at end of file +gemspec diff --git a/Guardfile b/Guardfile index 70b4a18..4a0ef87 100644 --- a/Guardfile +++ b/Guardfile @@ -2,4 +2,4 @@ guard 'rspec', cli: '--color --format nested' do watch(%r{^spec/.+_spec\.rb$}) watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } watch('spec/spec_helper.rb') { "spec" } -end \ No newline at end of file +end diff --git a/README.md b/README.md index 821bf25..ec295e0 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Bravo +# Bravo ![Travis status](https://travis-ci.org/leanucci/bravo.png) [![Gem Version](https://badge.fury.io/rb/bravo.png)](http://badge.fury.io/rb/bravo) [![Code Climate](https://codeclimate.com/repos/5292a01e89af7e473304513a/badges/4a29fbaff3d74a23e634/gpa.png)](https://codeclimate.com/repos/5292a01e89af7e473304513a/feed) @@ -53,20 +53,23 @@ Bravo no asume valores por defecto, por lo cual hay que configurar de forma expl Ejemplo de configuración tomado del spec_helper de Bravo: +```ruby - require 'bravo' +require 'bravo' - Bravo.pkey = 'spec/fixtures/certs/pkey' - Bravo.cert = 'spec/fixtures/certs/cert.crt' - Bravo.cuit = '20287740027' - Bravo.sale_point = '0002' - Bravo.default_concepto = 'Productos y Servicios' - Bravo.default_documento = 'CUIT' - Bravo.default_moneda = :peso - Bravo.own_iva_cond = :responsable_inscripto - Bravo.verbose = 'true' - Bravo.openssl_bin = '/usr/local/Cellar/openssl/1.0.1e/bin/openssl' - Bravo::AuthData.environment = :test +Bravo.pkey = 'spec/fixtures/certs/pkey' +Bravo.cert = 'spec/fixtures/certs/cert.crt' +Bravo.cuit = '20287740027' +Bravo.sale_point = '0002' +Bravo.default_concepto = 'Productos y Servicios' +Bravo.default_documento = 'CUIT' +Bravo.default_moneda = :peso +Bravo.own_iva_cond = :responsable_inscripto +Bravo.verbose = 'true' +Bravo.openssl_bin = '/usr/local/Cellar/openssl/1.0.1e/bin/openssl' +Bravo::AuthData.environment = :test + +``` ### Emisión de comprobantes @@ -88,18 +91,21 @@ Luego de configurar Bravo, autorizamos una factura: Código de ejemplo para la configuración anterior: +```ruby + +bill = Bravo::Bill.new - factura = Bravo::Bill.new +bill.net = 100.00 # el neto de la factura, total para Consumidor final +bill.aliciva_id = 2010 # define la alicuota de iva a utilizar, ver archivo constants. +bill.iva_cond = :consumidor_final # la condición ante el iva del comprador +bill.concepto = 'Servicios' # concepto de la factura +bill.invoice_type = :invoice # el tipo de comprobante a emitir, en este caso factura. - factura.net = 100.00 # el neto de la factura, total para Consumidor final - factura.aliciva_id = 2 # define la alicuota de iva a utilizar, ver archivo constants. - factura.iva_cond = :consumidor_final # la condición ante el iva del comprador - factura.concepto = 'Servicios' # concepto de la factura - factura.invoice_type = :invoice # el tipo de comprobante a emitir, en este caso factura. +bill.authorize - bill.authorize +bill.response.cae # contiene el cae para este comprobante. - bill.response.cae # contiene el cae para este comprobante. +``` ## TODO list diff --git a/autotest/discover.rb b/autotest/discover.rb index 42aea33..87dbf24 100644 --- a/autotest/discover.rb +++ b/autotest/discover.rb @@ -1 +1 @@ -Autotest.add_discovery { "rspec2" } #added according to rspec2 book \ No newline at end of file +Autotest.add_discovery { "rspec2" } # added according to rspec2 book diff --git a/bin/bravo b/bin/bravo index fa439cf..6b807f4 100755 --- a/bin/bravo +++ b/bin/bravo @@ -2,19 +2,23 @@ # -*- encoding: utf-8 -*- require 'thor' - +# base module module Bravo # Bravo executable for certificate request generation # class Bravo < Thor - desc 'gencsr', 'Crea el Certificate Signature Request' method_option :bin, type: :string, required: true, desc: 'El path completo al binario de openssl' - method_option :pkey, type: :string, desc: 'Path a una clave privada preexistente. Si se omite, se crea una clave en --out' - method_option :sn, type: :string, required: true, desc: 'Nombre del servidor. Sin uso práctico, es requerido por AFIP' - method_option :cn, type: :string, required: true, desc: 'Nombre de la compañía. Sin uso práctico, es requerido por AFIP' - method_option :cuit, type: :numeric, required: true, desc: 'Número de CUIT sin guiones. Ejemplo: 20876543217' - method_option :out, type: :string, default: 'bravo-certs', desc: 'Directorio de destino para los archivos creados. Si se omite, se crea el directorio bravo-certs en pwd' + method_option :pkey, type: :string, + desc: 'Path a una clave privada preexistente. Si se omite, se crea una clave en --out' + method_option :sn, type: :string, required: true, + desc: 'Nombre del servidor. Sin uso práctico, es requerido por AFIP' + method_option :cn, type: :string, required: true, + desc: 'Nombre de la compañía. Sin uso práctico, es requerido por AFIP' + method_option :cuit, type: :numeric, required: true, + desc: 'Número de CUIT sin guiones. Ejemplo: 20876543217' + method_option :out, type: :string, default: 'bravo-certs', + desc: 'Directorio de destino para los archivos creados. Default: "./bravo-certs"' # Certificate Signature Request wrapper for bravo. # @@ -28,7 +32,7 @@ module Bravo cuit = options[:cuit] out = options[:out] - Dir.mkdir(out) unless File.exists?(out) + Dir.mkdir(out) unless File.exist?(out) out_path = "#{ Dir.pwd }/#{ out }/" @@ -45,9 +49,10 @@ module Bravo end protected + # Creates a new private key # - def create_pkey(bin,out_path) + def create_pkey(bin, out_path) say('Creando pkey', :cyan) `#{ bin } genrsa -out #{ out_path }pkey 1024` say('Hecho!\n\n', :green) @@ -56,4 +61,4 @@ module Bravo end Bravo.start -end \ No newline at end of file +end diff --git a/bravo.gemspec b/bravo.gemspec index ccf1963..9c89c54 100644 --- a/bravo.gemspec +++ b/bravo.gemspec @@ -8,24 +8,25 @@ Gem::Specification.new do |gem| gem.version = Bravo::VERSION gem.authors = ["Leandro Marcucci"] gem.email = ["leanucci@gmail.com"] - gem.description = %q{Adaptador para el Web Service de Facturacion Electrónica de AFIP} - gem.summary = %q{Adaptador WSFE} + gem.description = 'Adaptador para el Web Service de Facturacion Electrónica de AFIP' + gem.summary = 'Adaptador WSFE' gem.homepage = "https://github.com/leanucci/bravo#readme" - gem.date = %q(2011-03-14) + gem.date = '2011-03-14' - gem.files = `git ls-files`.split($/) + gem.files = `git ls-files`.split($RS) gem.files.reject! { |f| f.include? 'vcr' } - gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } + gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) } gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) - gem.require_paths = ["lib", "bin"] + gem.require_paths = %w[lib bin] - gem.add_runtime_dependency(%q, ["~> 2.3.0"]) - gem.add_runtime_dependency(%q, ["~> 0.17.0"]) + gem.add_runtime_dependency(%{savon}, ["~> 2.8.1"]) + gem.add_runtime_dependency(%{thor}, ["~> 0.17"]) - gem.add_development_dependency(%q, ["~> 2.14.0"]) - gem.add_development_dependency(%q, ["~> 2.14.0"]) - gem.add_development_dependency(%q, ["~> 10.0.0"]) - gem.add_development_dependency(%q, ["~> 2.4.0"]) - gem.add_development_dependency(%q, ["~> 0.7.0"]) - gem.add_development_dependency(%q, ["~> 1.3.0"]) + gem.add_development_dependency(%{rspec}, ["~> 2.14.0"]) + gem.add_development_dependency(%{rspec-mocks}, ["~> 2.14.0"]) + gem.add_development_dependency(%{rake}, ["~> 10.0.0"]) + gem.add_development_dependency(%{vcr}, ["~> 2.4.0"]) + gem.add_development_dependency(%{simplecov}, ["~> 0.7.0"]) + gem.add_development_dependency(%{fakeweb}, ["~> 1.3.0"]) + gem.add_development_dependency(%{rubocop}, ["~> 0.26.1"]) end diff --git a/examples/invoices.rb b/examples/invoices.rb index 5b2d110..d0a2cd8 100644 --- a/examples/invoices.rb +++ b/examples/invoices.rb @@ -14,7 +14,7 @@ # Let's issue a Factura for 1200 ARS to a Responsable Inscripto bill_a = Bravo::Bill.new(iva_condition: :responsable_inscripto, net: 1200, invoice_type: :invoice) -bill_a.document_number = '30710151543' +bill_a.document_number = '30710151543' bill_a.document_type = 'CUIT' bill_a.authorize @@ -33,4 +33,3 @@ puts "Authorization result = #{ bill_b.authorized? }" puts "Authorization response." pp bill_b.response - diff --git a/lib/bravo.rb b/lib/bravo.rb index d06d62b..06cc1c4 100644 --- a/lib/bravo.rb +++ b/lib/bravo.rb @@ -16,7 +16,6 @@ class NullOrInvalidAttribute < StandardError; end # class MissingCertificate < StandardError; end - # This class handles the logging options # class Logger < Struct.new(:log, :pretty_xml, :level) @@ -25,13 +24,13 @@ class Logger < Struct.new(:log, :pretty_xml, :level) def initialize(opts = {}) self.log = opts[:log] || false - self.pretty_xml = opts[:pretty_xml] || self.log + self.pretty_xml = opts[:pretty_xml] || log self.level = opts[:level] || :debug end # @return [Hash] returns a hash with the proper logging optios for Savon. def logger_options - { log: self.log, pretty_print_xml: self.pretty_xml, log_level: self.level } + { log: log, pretty_print_xml: pretty_xml, log_level: level } end end @@ -44,9 +43,8 @@ def logger_options extend self - attr_accessor :cuit, :sale_point, :default_documento, :pkey, :cert, - :default_concepto, :default_moneda, :own_iva_cond, - :openssl_bin + attr_accessor :cuit, :sale_point, :default_documento, :pkey, :cert, :default_concepto, :default_moneda, + :own_iva_cond, :openssl_bin class << self # Receiver of the logging configuration options. @@ -69,7 +67,7 @@ def logger_options end def own_iva_cond=(iva_cond_symbol) - if Bravo::BILL_TYPE.has_key?(iva_cond_symbol) + if Bravo::BILL_TYPE.key?(iva_cond_symbol) @own_iva_cond = iva_cond_symbol else raise(NullOrInvalidAttribute.new, "El valor de own_iva_cond: (#{ iva_cond_symbol }) es inválido.") diff --git a/lib/bravo/auth_data.rb b/lib/bravo/auth_data.rb index 754ac7a..770da70 100644 --- a/lib/bravo/auth_data.rb +++ b/lib/bravo/auth_data.rb @@ -13,17 +13,10 @@ class << self # to be configured as Bravo.pkey and Bravo.cert # def fetch - unless File.exists?(Bravo.pkey) - raise "Archivo de llave privada no encontrado en #{ Bravo.pkey }" - end - - unless File.exists?(Bravo.cert) - raise "Archivo certificado no encontrado en #{ Bravo.cert }" - end + raise "Archivo de llave privada no encontrado en #{ Bravo.pkey }" unless File.exist?(Bravo.pkey) + raise "Archivo certificado no encontrado en #{ Bravo.cert }" unless File.exist?(Bravo.cert) - unless File.exists?(todays_data_file_name) - Bravo::Wsaa.login - end + Bravo::Wsaa.login unless File.exist?(todays_data_file_name) YAML.load_file(todays_data_file_name).each do |k, v| Bravo.const_set(k.to_s.upcase, v) unless Bravo.const_defined?(k.to_s.upcase) @@ -35,14 +28,14 @@ def fetch # def auth_hash fetch unless Bravo.constants.include?(:TOKEN) && Bravo.constants.include?(:SIGN) - { 'Token' => Bravo::TOKEN, 'Sign' => Bravo::SIGN, 'Cuit' => Bravo.cuit } + { 'Token' => Bravo::TOKEN, 'Sign' => Bravo::SIGN, 'Cuit' => Bravo.cuit } end # Returns the right wsaa url for the specific environment # @return [String] # def wsaa_url - raise 'Environment not sent to either :test or :production' unless Bravo::URLS.keys.include? environment + check_environment! Bravo::URLS[environment][:wsaa] end @@ -50,7 +43,7 @@ def wsaa_url # @return [String] # def wsfe_url - raise 'Environment not sent to either :test or :production' unless Bravo::URLS.keys.include? environment + check_environment! Bravo::URLS[environment][:wsfe] end @@ -60,6 +53,10 @@ def wsfe_url def todays_data_file_name @todays_data_file ||= "/tmp/bravo_#{ Bravo.cuit }_#{ Time.new.strftime('%Y_%m_%d') }.yml" end + + def check_environment! + raise 'Environment not set.' unless Bravo::URLS.keys.include? environment + end end end end diff --git a/lib/bravo/bill.rb b/lib/bravo/bill.rb index 9a8f0e2..696043a 100644 --- a/lib/bravo/bill.rb +++ b/lib/bravo/bill.rb @@ -9,20 +9,21 @@ class Bill # attr_reader :client - attr_accessor :net, :document_number, :iva_condition, :document_type, :concept, - :currency, :due_date, :aliciva_id, :date_from, :date_to, :body, :response, - :invoice_type + attr_accessor :net, :document_number, :iva_condition, :document_type, :concept, :currency, :due_date, + :aliciva_id, :date_from, :date_to, :body, :response, :invoice_type def initialize(attrs = {}) opts = { wsdl: Bravo::AuthData.wsfe_url }.merge! Bravo.logger_options - @client ||= Savon.client(opts) - @body = { 'Auth' => Bravo::AuthData.auth_hash } - @iva_condition = validate_iva_condition(attrs[:iva_condition]) - @net = attrs[:net] || 0 - @document_type = attrs[:document_type] || Bravo.default_documento - @currency = attrs[:currency] || Bravo.default_moneda - @concept = attrs[:concept] || Bravo.default_concepto - @invoice_type = validate_invoice_type(attrs[:invoice_type]) + @client ||= Savon.client(opts) + @body = { 'Auth' => Bravo::AuthData.auth_hash } + @iva_condition = validate_iva_condition(attrs[:iva_condition]) + @net = attrs[:net].to_f.round(2) || 0 + @document_type = attrs[:document_type] || Bravo.default_documento + @currency = attrs[:currency] || Bravo.default_moneda + @concept = attrs[:concept] || Bravo.default_concepto + @document_number = attrs[:document_number] + @document_type = attrs[:document_type] + @invoice_type = validate_invoice_type(attrs[:invoice_type]) end # Searches the corresponding invoice type according to the combination of @@ -38,7 +39,7 @@ def bill_type # @return [Float] the sum of both fields, or 0 if the net is 0. # def total - @total = net.zero? ? 0 : net + iva_sum + @total = net.zero? ? 0.0 : net + iva_sum end # Calculates the corresponding iva sum. @@ -61,7 +62,6 @@ def authorize # soap.namespaces['xmlns'] = 'http://ar.gov.afip.dif.FEV1/' soap.message body end - setup_response(response.to_hash) self.authorized? end @@ -70,38 +70,20 @@ def authorize # @return [Hash] returns the request body as a hash # def setup_bill - today = Time.new.strftime('%Y%m%d') - - fecaereq = { 'FeCAEReq' => { - 'FeCabReq' => Bravo::Bill.header(bill_type), - 'FeDetReq' => { - 'FECAEDetRequest' => { - 'Concepto' => Bravo::CONCEPTOS[concept], - 'DocTipo' => Bravo::DOCUMENTOS[document_type], - 'CbteFch' => today, - 'ImpTotConc' => 0.00, - 'MonId' => Bravo::MONEDAS[currency][:codigo], - 'MonCotiz' => 1, - 'ImpOpEx' => 0.00, - 'ImpTrib' => 0.00, - 'Iva' => { - 'AlicIva' => { - 'Id' => applicable_iva_code, - 'BaseImp' => net.round(2), - 'Importe' => iva_sum } } } } } } + fecaereq = setup_request_structure detail = fecaereq['FeCAEReq']['FeDetReq']['FECAEDetRequest'] detail['DocNro'] = document_number detail['ImpNeto'] = net.to_f detail['ImpIVA'] = iva_sum - detail['ImpTotal'] = total + detail['ImpTotal'] = total.round(2) detail['CbteDesde'] = detail['CbteHasta'] = Bravo::Reference.next_bill_number(bill_type) - unless concept == 0 - detail.merge!({ 'FchServDesde' => date_from || today, - 'FchServHasta' => date_to || today, - 'FchVtoPago' => due_date || today }) + unless Bravo::CONCEPTOS[concept] == '01' + detail.merge!('FchServDesde' => date_from || today, + 'FchServHasta' => date_to || today, + 'FchVtoPago' => due_date || today) end body.merge!(fecaereq) @@ -121,7 +103,7 @@ class << self # @return [Hash] # def header(bill_type) - # todo sacado de la factura + # toodo sacado de la factura { 'CantReg' => '1', 'CbteTipo' => bill_type, 'PtoVta' => Bravo.sale_point } end end @@ -129,9 +111,9 @@ def header(bill_type) # Response parser. Only works for the authorize method # @return [Struct] a struct with key-value pairs with the response values # + # rubocop:disable Metrics/MethodLength def setup_response(response) # TODO: turn this into an all-purpose Response class - result = response[:fecae_solicitar_response][:fecae_solicitar_result] response_header = result[:fe_cab_resp] @@ -140,27 +122,28 @@ def setup_response(response) request_header = body['FeCAEReq']['FeCabReq'].underscore_keys.symbolize_keys request_detail = body['FeCAEReq']['FeDetReq']['FECAEDetRequest'].underscore_keys.symbolize_keys - iva = request_detail.delete(:iva)['AlicIva'].underscore_keys.symbolize_keys + request_detail.merge!(request_detail.delete(:iva)['AlicIva'].underscore_keys.symbolize_keys) - request_detail.merge!(iva) + response_hash = { header_result: response_header.delete(:resultado), + authorized_on: response_header.delete(:fch_proceso), - response_hash = { :header_result => response_header.delete(:resultado), - :authorized_on => response_header.delete(:fch_proceso), - :detail_result => response_detail.delete(:resultado), - :cae_due_date => response_detail.delete(:cae_fch_vto), - :cae => response_detail.delete(:cae), - :iva_id => request_detail.delete(:id), - :iva_importe => request_detail.delete(:importe), - :moneda => request_detail.delete(:mon_id), - :cotizacion => request_detail.delete(:mon_cotiz), - :iva_base_imp => request_detail.delete(:base_imp), - :doc_num => request_detail.delete(:doc_nro) - }.merge!(request_header).merge!(request_detail) + detail_result: response_detail.delete(:resultado), + cae_due_date: response_detail.delete(:cae_fch_vto), + cae: response_detail.delete(:cae), - keys, values = response_hash.to_a.transpose + iva_id: request_detail.delete(:id), + iva_importe: request_detail.delete(:importe), + moneda: request_detail.delete(:mon_id), + cotizacion: request_detail.delete(:mon_cotiz), + iva_base_imp: request_detail.delete(:base_imp), + doc_num: request_detail.delete(:doc_nro) + }.merge!(request_header).merge!(request_detail) - self.response = (defined?(Struct::Response) ? Struct::Response : Struct.new('Response', *keys)).new(*values) + keys, values = response_hash.to_a.transpose + + self.response = Struct.new('Response', *keys).new(*values) end + # rubocop:enable Metrics/MethodLength def applicable_iva index = Bravo::APPLICABLE_IVA[Bravo.own_iva_cond][iva_condition] @@ -181,7 +164,7 @@ def validate_iva_condition(iva_cond) iva_cond else raise(NullOrInvalidAttribute.new, - "El valor de iva_condition debe estar incluído en #{ valid_conditions }") + "El valor de iva_condition debe estar incluído en #{ valid_conditions }") end end @@ -193,5 +176,22 @@ def validate_invoice_type(type) #{ Bravo::BILL_TYPE_A.keys }") end end + + def setup_request_structure + { 'FeCAEReq' => + { 'FeCabReq' => Bravo::Bill.header(bill_type), + 'FeDetReq' => + { 'FECAEDetRequest' => + { 'Concepto' => Bravo::CONCEPTOS[concept], 'DocTipo' => Bravo::DOCUMENTOS[document_type], + 'CbteFch' => today, 'ImpTotConc' => 0.00, 'MonId' => Bravo::MONEDAS[currency][:codigo], + 'MonCotiz' => 1, 'ImpOpEx' => 0.00, 'ImpTrib' => 0.00, + 'Iva' => + { 'AlicIva' => { 'Id' => applicable_iva_code, 'BaseImp' => net.round(2), + 'Importe' => iva_sum } } } } } } + end + + def today + Time.new.strftime('%Y%m%d') + end end end diff --git a/lib/bravo/constants.rb b/lib/bravo/constants.rb index 310d5c7..f0f875a 100644 --- a/lib/bravo/constants.rb +++ b/lib/bravo/constants.rb @@ -5,55 +5,56 @@ module Bravo # This constant contains the invoice types mappings between codes and names # used by WSFE. CBTE_TIPO = { - '01'=>'Factura A', - '02'=>'Nota de Débito A', - '03'=>'Nota de Crédito A', - '04'=>'Recibos A', - '05'=>'Notas de Venta al contado A', - '06'=>'Factura B', - '07'=>'Nota de Debito B', - '08'=>'Nota de Credito B', - '09'=>'Recibos B', - '10'=>'Notas de Venta al contado B', - '34'=>'Cbtes. A del Anexo I, Apartado A,inc.f),R.G.Nro. 1415', - '35'=>'Cbtes. B del Anexo I,Apartado A,inc. f),R.G. Nro. 1415', - '39'=>'Otros comprobantes A que cumplan con R.G.Nro. 1415', - '40'=>'Otros comprobantes B que cumplan con R.G.Nro. 1415', - '60'=>'Cta de Vta y Liquido prod. A', - '61'=>'Cta de Vta y Liquido prod. B', - '63'=>'Liquidacion A', - '64'=>'Liquidacion B' + '01' => 'Factura A', + '02' => 'Nota de Débito A', + '03' => 'Nota de Crédito A', + '04' => 'Recibos A', + '05' => 'Notas de Venta al contado A', + '06' => 'Factura B', + '07' => 'Nota de Debito B', + '08' => 'Nota de Credito B', + '09' => 'Recibos B', + '10' => 'Notas de Venta al contado B', + '34' => 'Cbtes. A del Anexo I, Apartado A,inc.f),R.G.Nro. 1415', + '35' => 'Cbtes. B del Anexo I,Apartado A,inc. f),R.G. Nro. 1415', + '39' => 'Otros comprobantes A que cumplan con R.G.Nro. 1415', + '40' => 'Otros comprobantes B que cumplan con R.G.Nro. 1415', + '60' => 'Cta de Vta y Liquido prod. A', + '61' => 'Cta de Vta y Liquido prod. B', + '63' => 'Liquidacion A', + '64' => 'Liquidacion B' } # Name to code mapping for Sale types. # - CONCEPTOS = { 'Productos'=>'01', 'Servicios'=>'02', 'Productos y Servicios'=>'03' } + CONCEPTOS = { 'Productos' => '01', 'Servicios' => '02', 'Productos y Servicios' => '03' } # Name to code mapping for types of documents. # 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' } + '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' + } # Currency code and names hash identified by a symbol # MONEDAS = { - :peso => { :codigo => 'PES', :nombre =>'Pesos Argentinos' }, - :dolar => { :codigo => 'DOL', :nombre =>'Dolar Estadounidense' }, - :real => { :codigo => '012', :nombre =>'Real' }, - :euro => { :codigo => '060', :nombre =>'Euro' }, - :oro => { :codigo => '049', :nombre =>'Gramos de Oro Fino' } } - + peso: { codigo: 'PES', nombre: 'Pesos Argentinos' }, + dolar: { codigo: 'DOL', nombre: 'Dolar Estadounidense' }, + real: { codigo: '012', nombre: 'Real' }, + euro: { codigo: '060', nombre: 'Euro' }, + oro: { codigo: '049', nombre: 'Gramos de Oro Fino' } + } # Tax percentage and codes according to each iva combination # @@ -62,7 +63,7 @@ module Bravo # Applicable tax according to buyer and seller's iva condition. # APPLICABLE_IVA = { - :responsable_inscripto => { + responsable_inscripto: { responsable_inscripto: 02, consumidor_final: 00, exento: 00, @@ -73,16 +74,18 @@ module Bravo # This hash keeps the codes for A document types by operation # BILL_TYPE_A = { - :invoice => '01', - :debit => '02', - :credit => '03' } + invoice: '01', + debit: '02', + credit: '03' + } # This hash keeps the codes for A document types by operation # BILL_TYPE_B = { - :invoice => '06', - :debit => '07', - :credit => '08' } + invoice: '06', + debit: '07', + credit: '08' + } # This hash keeps the different buyer and invoice type mapping corresponding to # the seller's iva condition and invoice kind. @@ -91,18 +94,20 @@ module Bravo # `BILL_TYPE[:responsable_inscripto][:responsable_inscripto][:invoice]` #=> '01' # BILL_TYPE = { - :responsable_inscripto => { - :responsable_inscripto => BILL_TYPE_A, - :consumidor_final => BILL_TYPE_B, - :exento => BILL_TYPE_B, - :responsable_monotributo => BILL_TYPE_B } } + responsable_inscripto: { + responsable_inscripto: BILL_TYPE_A, + consumidor_final: BILL_TYPE_B, + exento: BILL_TYPE_B, + responsable_monotributo: BILL_TYPE_B } + } # This hash keeps the set of urls for wsaa and wsfe for production and testing envs # URLS = { - :test => { :wsaa => 'https://wsaahomo.afip.gov.ar/ws/services/LoginCms', - :wsfe => 'https://wswhomo.afip.gov.ar/wsfev1/service.asmx?WSDL' }, + test: { wsaa: 'https://wsaahomo.afip.gov.ar/ws/services/LoginCms', + wsfe: 'https://wswhomo.afip.gov.ar/wsfev1/service.asmx?WSDL' }, - :production => { :wsaa => 'https://wsaa.afip.gov.ar/ws/services/LoginCms', - :wsfe => 'https://servicios1.afip.gov.ar/wsfev1/service.asmx' } } + production: { wsaa: 'https://wsaa.afip.gov.ar/ws/services/LoginCms', + wsfe: 'https://servicios1.afip.gov.ar/wsfev1/service.asmx?WSDL' } + } end diff --git a/lib/bravo/core_ext/string.rb b/lib/bravo/core_ext/string.rb index 5bc8bbc..42ee333 100644 --- a/lib/bravo/core_ext/string.rb +++ b/lib/bravo/core_ext/string.rb @@ -4,12 +4,12 @@ class String # Stolen from activesupport/lib/active_support/inflector/methods.rb, line 48 # def underscore - word = self.to_s.dup + word = to_s.dup word.gsub!(/::/, '/') - word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2') - word.gsub!(/([a-z\d])([A-Z])/,'\1_\2') + word.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2') + word.gsub!(/([a-z\d])([A-Z])/, '\1_\2') word.tr!('-', '_') word.downcase! word end -end \ No newline at end of file +end diff --git a/lib/bravo/reference.rb b/lib/bravo/reference.rb index 346cc41..f5b575a 100644 --- a/lib/bravo/reference.rb +++ b/lib/bravo/reference.rb @@ -9,7 +9,8 @@ def self.next_bill_number(cbte_type) set_client resp = @client.call(:fe_comp_ultimo_autorizado) do |soap| # soap.namespaces['xmlns'] = 'http://ar.gov.afip.dif.FEV1/' - soap.message 'Auth' => Bravo::AuthData.auth_hash, 'PtoVta' => Bravo.sale_point, 'CbteTipo' => cbte_type + soap.message 'Auth' => Bravo::AuthData.auth_hash, 'PtoVta' => Bravo.sale_point, + 'CbteTipo' => cbte_type end resp.to_hash[:fe_comp_ultimo_autorizado_response][:fe_comp_ultimo_autorizado_result][:cbte_nro].to_i + 1 diff --git a/lib/bravo/version.rb b/lib/bravo/version.rb index 8e1de9e..b8c9d27 100644 --- a/lib/bravo/version.rb +++ b/lib/bravo/version.rb @@ -1,5 +1,5 @@ module Bravo # Gem version # - VERSION = '1.0.0.rc2' + VERSION = '1.0.2' end diff --git a/lib/bravo/wsaa.rb b/lib/bravo/wsaa.rb index ad56c50..cab6c79 100644 --- a/lib/bravo/wsaa.rb +++ b/lib/bravo/wsaa.rb @@ -16,15 +16,15 @@ def self.login write_yaml(auth) end - protected # Builds the xml for the 'Ticket de Requerimiento de Acceso' # @return [String] containing the request body # + # rubocop:disable Metrics/MethodLength def self.build_tra - @now = (Time.now) - 120 - @from = @now.strftime('%FT%T%:z') - @to = (@now + ((12*60*60))).strftime('%FT%T%:z') - @id = @now.strftime('%s') + now = (Time.now) - 120 + @from = now.strftime('%FT%T%:z') + @to = (now + ((12 * 60 * 60))).strftime('%FT%T%:z') + @id = now.strftime('%s') tra = <<-EOF @@ -36,24 +36,24 @@ def self.build_tra wsfe EOF - return tra + tra end - + # rubocop:enable Metrics/MethodLength # Builds the CMS # @return [String] cms # def self.build_cms(tra) - cms = `echo '#{ tra }' | - #{ Bravo.openssl_bin } cms -sign -in /dev/stdin -signer #{ Bravo.cert } -inkey #{ Bravo.pkey } -nodetach \ - -outform der | + `echo '#{ tra }' | + #{ Bravo.openssl_bin } cms -sign -in /dev/stdin -signer #{ Bravo.cert } -inkey #{ Bravo.pkey } \ + -nodetach -outform der | #{ Bravo.openssl_bin } base64 -e` - return cms end # Builds the CMS request to log in to the server # @return [String] the cms body # def self.build_request(cms) + # rubocop:disable Metrics/LineLength request = <<-XML @@ -66,8 +66,9 @@ def self.build_request(cms) XML - return request + request end + # rubocop:enable Metrics/LineLength # Calls the WSAA with the request built by build_request # @return [Array] with the token and signature @@ -77,9 +78,9 @@ def self.call_wsaa(req) curl -k -s -H 'Content-Type: application/soap+xml; action=""' -d @- #{ Bravo::AuthData.wsaa_url }` response = CGI::unescapeHTML(response) - token = response.scan(/\(.+)\<\/token\>/).first.first - sign = response.scan(/\(.+)\<\/sign\>/).first.first - return [token, sign] + token = response.scan(%r{\(.+)\<\/token\>}).first.first + sign = response.scan(%r{\(.+)\<\/sign\>}).first.first + [token, sign] end # Writes the token and signature to a YAML file in the /tmp directory @@ -89,7 +90,7 @@ def self.write_yaml(certs) token: #{certs[0]} sign: #{certs[1]} YML - `echo '#{ yml }' > /tmp/bravo_#{ Bravo.cuit }_#{ Time.new.strftime('%Y_%m_%d') }.yml` + `echo '#{ yml }' > /tmp/bravo_#{ Bravo.cuit }_#{ Time.new.strftime('%Y_%m_%d') }.yml` end end diff --git a/spec/bravo/auth_data_spec.rb b/spec/bravo/auth_data_spec.rb index 3ba4784..1d281e7 100644 --- a/spec/bravo/auth_data_spec.rb +++ b/spec/bravo/auth_data_spec.rb @@ -10,4 +10,4 @@ Bravo.constants.should include(:TOKEN, :SIGN) end end -end \ No newline at end of file +end diff --git a/spec/bravo/bill_spec.rb b/spec/bravo/bill_spec.rb index 058637a..6f82e43 100644 --- a/spec/bravo/bill_spec.rb +++ b/spec/bravo/bill_spec.rb @@ -7,23 +7,23 @@ describe '.header' do it 'sets up the header hash' do @header = Bravo::Bill.header(0) - @header.size.should == 3 - ['CantReg', 'CbteTipo', 'PtoVta'].each do |key| - @header.has_key?(key).should == true + expect(@header.size).to be 3 + %w[CantReg CbteTipo PtoVta].each do |key| + expect(@header.key?(key)).to be_true end end end describe '.initialize' do it 'applies Bravos defaults' do - bill.client.class.name.should == 'Savon::Client' + expect(bill.client).to be_a Savon::Client - ['Token', 'Sign', 'Cuit'].each do |key| - bill.body['Auth'][key].should_not == nil + %w[Token Sign Cuit].each do |key| + expect(bill.body['Auth'].fetch(key, nil)).not_to be_nil end - bill.document_type.should == Bravo.default_documento - bill.currency.should == Bravo.default_moneda + expect(bill.document_type).to be Bravo.default_documento + expect(bill.currency).to be Bravo.default_moneda end end @@ -32,13 +32,13 @@ it 'returns the bill type for Responsable Inscripto' do bill.iva_condition = :responsable_inscripto - bill.bill_type.should == '01' + expect(bill.bill_type).to eq '01' end it 'returns the bill type for Consumidor Final' do bill.iva_condition = :consumidor_final - bill.bill_type.should == '06' + expect(bill.bill_type).to eq '06' end end @@ -49,8 +49,8 @@ bill.net = 100.89 bill.aliciva_id = 2 - bill.iva_sum.should be_within(0.005).of(21.19) - bill.total.should be_within(0.005).of(122.08) + expect(bill.iva_sum).to be_within(0.005).of(21.19) + expect(bill.total).to be_within(0.005).of(122.08) end end @@ -63,14 +63,15 @@ bill.concept = 'Servicios' end - it 'uses today dates when due and service dates are ommitted', vcr: { cassette_name: 'setup_bill_ommitted_date' } do + it 'uses today dates when due and service dates are null', + vcr: { cassette_name: 'setup_bill_ommitted_date' } do bill.setup_bill detail = bill.body['FeCAEReq']['FeDetReq']['FECAEDetRequest'] - detail['FchServDesde'].should == Time.new.strftime('%Y%m%d') - detail['FchServHasta'].should == Time.new.strftime('%Y%m%d') - detail['FchVtoPago'].should == Time.new.strftime('%Y%m%d') + expect(detail['FchServDesde']).to eq Time.new.strftime('%Y%m%d') + expect(detail['FchServHasta']).to eq Time.new.strftime('%Y%m%d') + expect(detail['FchVtoPago']).to eq Time.new.strftime('%Y%m%d') end it 'uses given due and service dates', vcr: { cassette_name: 'setup_bill_given_date' } do @@ -82,34 +83,35 @@ detail = bill.body['FeCAEReq']['FeDetReq']['FECAEDetRequest'] - detail['FchServDesde'].should == '20111101' - detail['FchServHasta'].should == '20111130' - detail['FchVtoPago'].should == '20111210' + expect(detail['FchServDesde']).to eq '20111101' + expect(detail['FchServHasta']).to eq '20111130' + expect(detail['FchVtoPago']).to eq '20111210' end end describe '#authorize' do describe 'for facturas' do Bravo::BILL_TYPE[Bravo.own_iva_cond].keys.each do |target_iva_cond| - describe "issued to #{ target_iva_cond.to_s }" do + describe "issued to #{ target_iva_cond }" do Bravo::BILL_TYPE[Bravo.own_iva_cond][target_iva_cond].keys.each do |bill_type| - vcr_options = { cassette_name: "#{ target_iva_cond.to_s }_and_#{ bill_type }" } + vcr_options = { cassette_name: "#{ target_iva_cond }_and_#{ bill_type }" } it "authorizes bill type #{ bill_type }", vcr: vcr_options do - bill.net = 10000.88 - bill.aliciva_id = 2 - bill.document_number = '30710151543' - bill.iva_condition = target_iva_cond - bill.concept = 'Servicios' + bill.net = 10_000.88 + bill.aliciva_id = 2 + bill.document_number = '30710151543' + bill.iva_condition = target_iva_cond + bill.concept = 'Servicios' bill.invoice_type = bill_type - bill.authorized?.should == false - bill.authorize.should == true - bill.authorized?.should == true + expect(bill.authorized?).to be_false + + expect(bill.authorize).to be_true + expect(bill.authorized?).to be_true response = bill.response - response.length.should == 28 - response.cae.length.should == 14 + expect(response.length).to eql 28 + expect(response.cae.length).to eql 14 end end end diff --git a/spec/bravo/reference_spec.rb b/spec/bravo/reference_spec.rb index 7942975..2e0833b 100644 --- a/spec/bravo/reference_spec.rb +++ b/spec/bravo/reference_spec.rb @@ -9,4 +9,4 @@ # # # end -end \ No newline at end of file +end diff --git a/spec/bravo/wsaa_spec.rb b/spec/bravo/wsaa_spec.rb index ffb97a9..bbd0824 100644 --- a/spec/bravo/wsaa_spec.rb +++ b/spec/bravo/wsaa_spec.rb @@ -4,7 +4,7 @@ before do @now = (Time.now) - 120 @from = @now.strftime('%FT%T%:z') - @to = (@now + ((12*60*60))).strftime('%FT%T%:z') + @to = (@now + ((12 * 60 * 60))).strftime('%FT%T%:z') @id = @now.strftime('%s') @tra = <<-EOF diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index cd50de1..bb0ead0 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,4 +1,4 @@ -$:.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) +$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) require 'bravo' require 'rspec' require 'vcr' @@ -8,6 +8,7 @@ begin require 'debugger' rescue LoadError + puts 'debugger not found' end VCR.configure do |c| @@ -18,7 +19,7 @@ RSpec.configure do |config| config.treat_symbols_as_metadata_keys_with_true_values = true - config.filter_run :focus => true + config.filter_run focus: true config.run_all_when_everything_filtered = true end @@ -30,17 +31,14 @@ Bravo.default_documento = 'CUIT' Bravo.default_moneda = :peso Bravo.own_iva_cond = :responsable_inscripto -# Bravo.logger = { log: true, level: :critical } -Bravo.openssl_bin = ENV["TRAVIS"] ? 'openssl' : '/usr/local/Cellar/openssl/1.0.1e/bin/openssl' -Bravo::AuthData.environment = :test +Bravo.logger = { log: false, level: :debug } +Bravo.openssl_bin = 'openssl' +Bravo::AuthData.environment = :test # TODO: refactor into actual validations -unless Bravo.cuit - raise(Bravo::NullOrInvalidAttribute.new, 'Please set CUIT env variable.') -end + +raise(Bravo::NullOrInvalidAttribute.new, 'Please set CUIT env variable.') unless Bravo.cuit [Bravo.pkey, Bravo.cert].each do |file| - unless File.exists?("#{ file }") - raise(Bravo::MissingCertificate.new, "No existe #{ file }") - end + raise(Bravo::MissingCertificate.new, "No existe #{ file }") unless File.exist?("#{ file }") end