require 'net/http' require 'net/https' require 'active_merchant/billing/response' module ActiveMerchant #:nodoc: module Billing #:nodoc: # # == Description # The Gateway class is the base class for all ActiveMerchant gateway implementations. # # The standard list of gateway functions that most concrete gateway subclasses implement is: # # * purchase(money, credit_card, options = {}) # * authorize(money, credit_card, options = {}) # * capture(money, authorization, options = {}) # * void(identification, options = {}) # * refund(money, identification, options = {}) # * verify(credit_card, options = {}) # # Some gateways also support features for storing credit cards: # # * store(credit_card, options = {}) # * unstore(identification, options = {}) # # === Gateway Options # The options hash consists of the following options: # # * :order_id - The order number # * :ip - The IP address of the customer making the purchase # * :customer - The name, customer number, or other information that identifies the customer # * :invoice - The invoice number # * :merchant - The name or description of the merchant offering the product # * :description - A description of the transaction # * :email - The email address of the customer # * :currency - The currency of the transaction. Only important when you are using a currency that is not the default with a gateway that supports multiple currencies. # * :billing_address - A hash containing the billing address of the customer. # * :shipping_address - A hash containing the shipping address of the customer. # # The :billing_address, and :shipping_address hashes can have the following keys: # # * :name - The full name of the customer. # * :company - The company name of the customer. # * :address1 - The primary street address of the customer. # * :address2 - Additional line of address information. # * :city - The city of the customer. # * :state - The state of the customer. The 2 digit code for US and Canadian addresses. The full name of the state or province for foreign addresses. # * :country - The [ISO 3166-1-alpha-2 code](http://www.iso.org/iso/country_codes/iso_3166_code_lists/english_country_names_and_code_elements.htm) for the customer. # * :zip - The zip or postal code of the customer. # * :phone - The phone number of the customer. # # == Implementing new gateways # # See the {ActiveMerchant Guide to Contributing}[https://github.com/activemerchant/active_merchant/wiki/Contributing] # class Gateway include PostsData include CreditCardFormatting DEBIT_CARDS = [ :switch, :solo ] CREDIT_DEPRECATION_MESSAGE = "Support for using credit to refund existing transactions is deprecated and will be removed from a future release of ActiveMerchant. Please use the refund method instead." RECURRING_DEPRECATION_MESSAGE = "Recurring functionality in ActiveMerchant is deprecated and will be removed in a future version. Please contact the ActiveMerchant maintainers if you have an interest in taking ownership of a separate gem that continues support for it." # == Standardized Error Codes # # :incorrect_number - Card number does not comply with ISO/IEC 7812 numbering standard # :invalid_number - Card number was not matched by processor # :invalid_expiry_date - Expiry date does not match correct formatting # :invalid_cvc - Security codes does not match correct format (3-4 digits) # :expired_card - Card number is expired # :incorrect_cvc - Security code was not matched by the processor # :incorrect_zip - Zip code is not in correct format # :incorrect_address - Billing address info was not matched by the processor # :incorrect_pin - Card PIN is incorrect # :card_declined - Card number declined by processor # :processing_error - Processor error # :call_issuer - Transaction requires voice authentication, call issuer # :pickup_card - Issuer requests that you pickup the card from merchant # :test_mode_live_card - Card was declined. Request was in test mode, but used a non test card. # :unsupported_feature - Transaction failed due to gateway or merchant # configuration not supporting a feature used, such # as network tokenization. STANDARD_ERROR_CODE = { :incorrect_number => 'incorrect_number', :invalid_number => 'invalid_number', :invalid_expiry_date => 'invalid_expiry_date', :invalid_cvc => 'invalid_cvc', :expired_card => 'expired_card', :incorrect_cvc => 'incorrect_cvc', :incorrect_zip => 'incorrect_zip', :incorrect_address => 'incorrect_address', :incorrect_pin => 'incorrect_pin', :card_declined => 'card_declined', :processing_error => 'processing_error', :call_issuer => 'call_issuer', :pickup_card => 'pick_up_card', :config_error => 'config_error', :test_mode_live_card => 'test_mode_live_card', :unsupported_feature => 'unsupported_feature', } cattr_reader :implementations @@implementations = [] def self.inherited(subclass) super @@implementations << subclass end def generate_unique_id SecureRandom.hex(16) end # The format of the amounts used by the gateway # :dollars => '12.50' # :cents => '1250' class_attribute :money_format self.money_format = :dollars # The default currency for the transactions if no currency is provided class_attribute :default_currency # The supported card types for the gateway class_attribute :supported_cardtypes self.supported_cardtypes = [] class_attribute :currencies_without_fractions, :currencies_with_three_decimal_places self.currencies_without_fractions = %w(BIF BYR CLP CVE DJF GNF ISK JPY KMF KRW PYG RWF UGX VND VUV XAF XOF XPF) self.currencies_with_three_decimal_places = %w() class_attribute :homepage_url class_attribute :display_name class_attribute :test_url, :live_url class_attribute :abstract_class self.abstract_class = false # The application making the calls to the gateway # Useful for things like the PayPal build notation (BN) id fields class_attribute :application_id, instance_writer: false self.application_id = nil attr_reader :options # Use this method to check if your gateway of interest supports a credit card of some type def self.supports?(card_type) supported_cardtypes.include?(card_type.to_sym) end def self.card_brand(source) result = source.respond_to?(:brand) ? source.brand : source.type result.to_s.downcase end def self.supported_countries=(country_codes) country_codes.each do |country_code| unless ActiveMerchant::Country.find(country_code) raise ActiveMerchant::InvalidCountryCodeError, "No country could be found for the country #{country_code}" end end @supported_countries = country_codes.dup end def self.supported_countries @supported_countries ||= [] end def supported_countries self.class.supported_countries end def card_brand(source) self.class.card_brand(source) end # Initialize a new gateway. # # See the documentation for the gateway you will be using to make sure there are no other # required options. def initialize(options = {}) @options = options end # Are we running in test mode? def test? (@options.has_key?(:test) ? @options[:test] : Base.test?) end # Does this gateway know how to scrub sensitive information out of HTTP transcripts? def supports_scrubbing? false end def scrub(transcript) raise RuntimeError.new("This gateway does not support scrubbing.") end def supports_network_tokenization? false end protected # :nodoc: all def normalize(field) case field when "true" then true when "false" then false when "" then nil when "null" then nil else field end end def user_agent @@ua ||= JSON.dump({ :bindings_version => ActiveMerchant::VERSION, :lang => 'ruby', :lang_version => "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})", :platform => RUBY_PLATFORM, :publisher => 'active_merchant' }) end def strip_invalid_xml_chars(xml) begin REXML::Document.new(xml) rescue REXML::ParseException xml = xml.gsub(/&(?!(?:[a-z]+|#[0-9]+|x[a-zA-Z0-9]+);)/, '&') end xml end private # :nodoc: all def name self.class.name.scan(/\:\:(\w+)Gateway/).flatten.first end def amount(money) return nil if money.nil? cents = if money.respond_to?(:cents) ActiveMerchant.deprecated "Support for Money objects is deprecated and will be removed from a future release of ActiveMerchant. Please use an Integer value in cents" money.cents else money end if money.is_a?(String) raise ArgumentError, 'money amount must be a positive Integer in cents.' end if self.money_format == :cents cents.to_s else sprintf("%.2f", cents.to_f / 100) end end def non_fractional_currency?(currency) self.currencies_without_fractions.include?(currency.to_s) end def three_decimal_currency?(currency) self.currencies_with_three_decimal_places.include?(currency.to_s) end def localized_amount(money, currency) amount = amount(money) return amount unless non_fractional_currency?(currency) || three_decimal_currency?(currency) if non_fractional_currency?(currency) if self.money_format == :cents sprintf("%.0f", amount.to_f / 100) else amount.split('.').first end elsif three_decimal_currency?(currency) if self.money_format == :cents amount.to_s else sprintf("%.3f", (amount.to_f / 10)) end end end def currency(money) money.respond_to?(:currency) ? money.currency : self.default_currency end def truncate(value, max_size) return nil unless value value.to_s[0, max_size] end def split_names(full_name) names = (full_name || "").split return [nil, nil] if names.size == 0 last_name = names.pop first_name = names.join(" ") [first_name, last_name] end def requires_start_date_or_issue_number?(credit_card) return false if card_brand(credit_card).blank? DEBIT_CARDS.include?(card_brand(credit_card).to_sym) end def requires!(hash, *params) params.each do |param| if param.is_a?(Array) raise ArgumentError.new("Missing required parameter: #{param.first}") unless hash.has_key?(param.first) valid_options = param[1..-1] raise ArgumentError.new("Parameter: #{param.first} must be one of #{valid_options.to_sentence(:words_connector => 'or')}") unless valid_options.include?(hash[param.first]) else raise ArgumentError.new("Missing required parameter: #{param}") unless hash.has_key?(param) end end end end end end