<commit_before>require File.dirname(__FILE__) + '/orbital/orbital_soft_descriptors.rb'
require "rexml/document"

module ActiveMerchant #:nodoc:
  module Billing #:nodoc:
    # For more information on Orbital, visit the {integration center}[http://download.chasepaymentech.com]
    #     
    # ==== Authentication Options
    # 
    # The Orbital Gateway supports two methods of authenticating incoming requests:
    # Source IP authentication and Connection Username/Password authentication
    # 
    # In addition, these IP addresses/Connection Usernames must be affiliated with the Merchant IDs 
    # for which the client should be submitting transactions.
    # 
    # This does allow Third Party Hosting service organizations presenting on behalf of other 
    # merchants to submit transactions.  However, each time a new customer is added, the 
    # merchant or Third-Party hosting organization needs to ensure that the new Merchant IDs 
    # or Chain IDs are affiliated with the hosting companies IPs or Connection Usernames.
    # 
    # If the merchant expects to have more than one merchant account with the Orbital 
    # Gateway, it should have its IP addresses/Connection Usernames affiliated at the Chain 
    # level hierarchy within the Orbital Gateway.  Each time a new merchant ID is added, as
    # long as it is placed within the same Chain, it will simply work.  Otherwise, the additional 
    # MIDs will need to be affiliated with the merchant IPs or Connection Usernames respectively.
    # For example, we generally affiliate all Salem accounts [BIN 000001] with 
    # their Company Number [formerly called MA #] number so all MIDs or Divisions under that 
    # Company will automatically be affiliated.
    
    class OrbitalGateway < Gateway
      API_VERSION = "4.6"
      
      POST_HEADERS = {
        "MIME-Version" => "1.0",
        "Content-Type" => "Application/PTI46",
        "Content-transfer-encoding" => "text",
        "Request-number" => '1',
        "Document-type" => "Request",
        "Interface-Version" => "Ruby|ActiveMerchant|Proprietary Gateway"
      }
      
      SUCCESS, APPROVED = '0', '00'
      
      class_attribute :primary_test_url, :secondary_test_url, :primary_live_url, :secondary_live_url
      
      self.primary_test_url = "https://orbitalvar1.paymentech.net/authorize"
      self.secondary_test_url = "https://orbitalvar2.paymentech.net/authorize"
      
      self.primary_live_url = "https://orbital1.paymentech.net/authorize"
      self.secondary_live_url = "https://orbital2.paymentech.net/authorize"
      
      self.supported_countries = ["US", "CA"]
      self.default_currency = "CAD"
      self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club, :jcb]
      
      self.display_name = '<NAME>'
      self.homepage_url = 'http://chasepaymentech.com/'
      
      self.money_format = :cents
            
      CURRENCY_CODES = { 
        "AUD" => '036',
        "CAD" => '124',
        "CZK" => '203',
        "DKK" => '208',
        "HKD" => '344',
        "ICK" => '352',
        "JPY" => '392',
        "MXN" => '484',
        "NZD" => '554',
        "NOK" => '578',
        "SGD" => '702',
        "SEK" => '752',
        "CHF" => '756',
        "GBP" => '826',
        "USD" => '840',
        "EUR" => '978'
      }

      def initialize(options = {})
        requires!(options, :merchant_id)
        requires!(options, :login, :password) unless options[:ip_authentication]
        @options = options
        super
      end
      
      # A – Authorization request
      def authorize(money, creditcard, options = {})
        order = build_new_order_xml('A', money, options) do |xml|
          add_creditcard(xml, creditcard, options[:currency])        
          add_address(xml, creditcard, options)   
        end
        commit(order)
      end
      
      # AC – Authorization and Capture
      def purchase(money, creditcard, options = {})
        order = build_new_order_xml('AC', money, options) do |xml|
          add_creditcard(xml, creditcard, options[:currency])
          add_address(xml, creditcard, options)   
        end
        commit(order)
      end                       
      
      # MFC - Mark For Capture
      def capture(money, authorization, options = {})
        commit(build_mark_for_capture_xml(money, authorization, options))
      end
      
      # R – Refund request
      def refund(money, authorization, options = {})
        order = build_new_order_xml('R', money, options.merge(:authorization => authorization)) do |xml|
          add_refund(xml, options[:currency])
        end
        commit(order)
      end

      def credit(money, authorization, options= {})
        deprecated CREDIT_DEPRECATION_MESSAGE
        refund(money, authorization, options)
      end
      
      # setting money to nil will perform a full void
      def void(authorization, options = {})
        order = build_void_request_xml(authorization, options)
        commit(order)
      end
    
      private                       
            
      def add_customer_data(xml, options)
        if options[:customer_ref_num]
          xml.tag! :CustomerProfileFromOrderInd, 'S'
          xml.tag! :CustomerRefNum, options[:customer_ref_num]
        else
          xml.tag! :CustomerProfileFromOrderInd, 'A'
        end
      end
      
      def add_soft_descriptors(xml, soft_desc)
        xml.tag! :SDMerchantName, soft_desc.merchant_name
        xml.tag! :SDProductDescription, soft_desc.product_description
        xml.tag! :SDMerchantCity, soft_desc.merchant_city
        xml.tag! :SDMerchantPhone, soft_desc.merchant_phone
        xml.tag! :SDMerchantURL, soft_desc.merchant_url
        xml.tag! :SDMerchantEmail, soft_desc.merchant_email
      end

      def add_address(xml, creditcard, options)      
        if address = options[:billing_address] || options[:address]
          xml.tag! :AVSzip, address[:zip]
          xml.tag! :AVSaddress1, address[:address1]
          xml.tag! :AVSaddress2, address[:address2]
          xml.tag! :AVScity, address[:city]
          xml.tag! :AVSstate, address[:state]
          xml.tag! :AVSphoneNum, address[:phone] ? address[:phone].scan(/\d/).join.to_s : nil
          xml.tag! :AVSname, creditcard.name
          xml.tag! :AVScountryCode, address[:country]
        end
      end

      def add_creditcard(xml, creditcard, currency=nil)
        xml.tag! :AccountNum, creditcard.number
        xml.tag! :Exp, expiry_date(creditcard)
        
        xml.tag! :CurrencyCode, currency_code(currency)
        xml.tag! :CurrencyExponent, '2' # Will need updating to support currencies such as the Yen.
        
        xml.tag! :CardSecValInd, 1 if creditcard.verification_value? && %w( visa discover ).include?(creditcard.brand)
        xml.tag! :CardSecVal,  creditcard.verification_value if creditcard.verification_value?
      end
      
      def add_refund(xml, currency=nil)
        xml.tag! :AccountNum, nil
        
        xml.tag! :CurrencyCode, currency_code(currency)
        xml.tag! :CurrencyExponent, '2' # Will need updating to support currencies such as the Yen.
      end
      
      def parse(body)
        response = {}
        xml = REXML::Document.new(body)
        root = REXML::XPath.first(xml, "//Response") ||
               REXML::XPath.first(xml, "//ErrorResponse")
        if root
          root.elements.to_a.each do |node|
            recurring_parse_element(response, node)
          end
        end
        response
      end     
      
      def recurring_parse_element(response, node)
        if node.has_elements?
          node.elements.each{|e| recurring_parse_element(response, e) }
        else
          response[node.name.underscore.to_sym] = node.text
        end
      end
      
      def commit(order)
        headers = POST_HEADERS.merge("Content-length" => order.size.to_s)
        request = lambda {return parse(ssl_post(remote_url, order, headers))}
        
        # Failover URL will be used in the event of a connection error
        begin response = request.call; rescue ConnectionError; retry end
        
        Response.new(success?(response), message_from(response), response,
          {:authorization => "#{response[:tx_ref_num]};#{response[:order_id]}",
           :test => self.test?,
           :avs_result => {:code => response[:avs_resp_code]},
           :cvv_result => response[:cvv2_resp_code]
          }
        )
      end
      
      def remote_url
        unless $!.class == ActiveMerchant::ConnectionError
          self.test? ? self.primary_test_url : self.primary_live_url
        else
          self.test? ? self.secondary_test_url : self.secondary_live_url
        end
      end

      def success?(response)
        if response[:message_type].nil? || response[:message_type] == "R"
          response[:proc_status] == SUCCESS
        else
          response[:proc_status] == SUCCESS &&
            response[:resp_code] == APPROVED
        end
      end
      
      def message_from(response)
        success?(response) ? 'APPROVED' : response[:resp_msg] || response[:status_msg]
      end
      
      def ip_authentication?
        @options[:ip_authentication] == true
      end

      def build_new_order_xml(action, money, parameters = {})
        requires!(parameters, :order_id)
        xml = xml_envelope
        xml.tag! :Request do
          xml.tag! :NewOrder do
            add_xml_credentials(xml)
            xml.tag! :IndustryType, parameters[:industry_type] || "EC"
            xml.tag! :MessageType, action
            add_bin_merchant_and_terminal(xml, parameters)
            
            yield xml if block_given?
            
            xml.tag! :Comments, parameters[:comments] if parameters[:comments]
            xml.tag! :OrderID, format_order_id(parameters[:order_id])
            xml.tag! :Amount, amount(money)
            
            # Append Transaction Reference Number at the end for Refund transactions
            if action == "R"
              tx_ref_num, _ = parameters[:authorization].split(';')
              xml.tag! :TxRefNum, tx_ref_num
            end
          end
        end
        xml.target!
      end
      
      def build_mark_for_capture_xml(money, authorization, parameters = {})
        tx_ref_num, order_id = authorization.split(';')
        xml = xml_envelope
        xml.tag! :Request do
          xml.tag! :MarkForCapture do
            add_xml_credentials(xml)
            xml.tag! :OrderID, order_id
            xml.tag! :Amount, amount(money)
            add_bin_merchant_and_terminal(xml, parameters)
            xml.tag! :TxRefNum, tx_ref_num
          end
        end
        xml.target!
      end
      
      def build_void_request_xml(authorization, parameters = {})
        tx_ref_num, order_id = authorization.split(';')
        xml = xml_envelope
        xml.tag! :Request do
          xml.tag! :Reversal do
            add_xml_credentials(xml)
            xml.tag! :TxRefNum, tx_ref_num
            xml.tag! :TxRefIdx, parameters[:transaction_index]
            xml.tag! :OrderID, order_id
            add_bin_merchant_and_terminal(xml, parameters)
          end
        end
        xml.target!
      end
      
      def currency_code(currency)
        CURRENCY_CODES[(currency || self.default_currency)].to_s
      end
      
      def expiry_date(credit_card)
        "#{format(credit_card.month, :two_digits)}#{format(credit_card.year, :two_digits)}"
      end

      def bin
        @options[:bin] || (salem_mid? ? '000001' : '000002')
      end

      def xml_envelope
        xml = Builder::XmlMarkup.new(:indent => 2)
        xml.instruct!(:xml, :version => '1.0', :encoding => 'UTF-8')
        xml
      end

      def add_xml_credentials(xml)
        xml.tag! :OrbitalConnectionUsername, @options[:login] unless ip_authentication?
        xml.tag! :OrbitalConnectionPassword, @options[:password] unless ip_authentication?
      end

      def add_bin_merchant_and_terminal(xml, parameters)
        xml.tag! :BIN, bin
        xml.tag! :MerchantID, @options[:merchant_id]
        xml.tag! :TerminalID, parameters[:terminal_id] || '001'
      end

      def salem_mid?
        @options[:merchant_id].length == 6
      end

      # The valid characters include:
      #
      # 1. all letters and digits
      # 2. - , $ @ & and a space character, though the space character cannot be the leading character
      # 3. PINless Debit transactions can only use uppercase and lowercase alpha (A-Z, a-z) and numeric (0-9)
      def format_order_id(order_id)
        illegal_characters = /[^,$@\- \w]/
        order_id = order_id.to_s.gsub(/\./, '-')
        order_id.gsub!(illegal_characters, '')
        order_id[0...22]
      end
    end
  end
end<commit_msg>Added card storage and recurring billing for Orbital Paymentech gateway<commit_after>require File.dirname(__FILE__) + '/orbital/orbital_soft_descriptors.rb'
require "rexml/document"
require"date"

module ActiveMerchant #:nodoc:
  module Billing #:nodoc:
    # For more information on Orbital, visit the {integration center}[http://download.chasepaymentech.com]
    #     
    # ==== Authentication Options
    # 
    # The Orbital Gateway supports two methods of authenticating incoming requests:
    # Source IP authentication and Connection Username/Password authentication
    # 
    # In addition, these IP addresses/Connection Usernames must be affiliated with the Merchant IDs 
    # for which the client should be submitting transactions.
    # 
    # This does allow Third Party Hosting service organizations presenting on behalf of other 
    # merchants to submit transactions.  However, each time a new customer is added, the 
    # merchant or Third-Party hosting organization needs to ensure that the new Merchant IDs 
    # or Chain IDs are affiliated with the hosting companies IPs or Connection Usernames.
    # 
    # If the merchant expects to have more than one merchant account with the Orbital 
    # Gateway, it should have its IP addresses/Connection Usernames affiliated at the Chain 
    # level hierarchy within the Orbital Gateway.  Each time a new merchant ID is added, as
    # long as it is placed within the same Chain, it will simply work.  Otherwise, the additional 
    # MIDs will need to be affiliated with the merchant IPs or Connection Usernames respectively.
    # For example, we generally affiliate all Salem accounts [BIN 000001] with 
    # their Company Number [formerly called MA #] number so all MIDs or Divisions under that 
    # Company will automatically be affiliated.
    
    class OrbitalGateway < Gateway
      API_VERSION = "4.6"
      
      POST_HEADERS = {
        "MIME-Version" => "1.0",
        "Content-Type" => "Application/PTI46",
        "Content-transfer-encoding" => "text",
        "Request-number" => '1',
        "Document-type" => "Request",
        "Interface-Version" => "Ruby|ActiveMerchant|Proprietary Gateway"
      }
      
      SUCCESS, APPROVED = '0', '00'
      
      class_attribute :primary_test_url, :secondary_test_url, :primary_live_url, :secondary_live_url
      
      self.primary_test_url = "https://orbitalvar1.paymentech.net/authorize"
      self.secondary_test_url = "https://orbitalvar2.paymentech.net/authorize"
      
      self.primary_live_url = "https://orbital1.paymentech.net/authorize"
      self.secondary_live_url = "https://orbital2.paymentech.net/authorize"
      
      self.supported_countries = ["US", "CA"]
      self.default_currency = "CAD"
      self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club, :jcb]
      
      self.display_name = 'Orbital Paymentech'
      self.homepage_url = 'http://chasepaymentech.com/'
      
      self.money_format = :cents
            
      CURRENCY_CODES = { 
        "AUD" => '036',
        "CAD" => '124',
        "CZK" => '203',
        "DKK" => '208',
        "HKD" => '344',
        "ICK" => '352',
        "JPY" => '392',
        "MXN" => '484',
        "NZD" => '554',
        "NOK" => '578',
        "SGD" => '702',
        "SEK" => '752',
        "CHF" => '756',
        "GBP" => '826',
        "USD" => '840',
        "EUR" => '978'
      }

      def initialize(options = {})
        requires!(options, :merchant_id)
        requires!(options, :login, :password) unless options[:ip_authentication]
        @options = options
        super
      end
      
      # A – Authorization request
      def authorize(money, creditcard_or_billing_id, options = {})
        order = build_new_order_xml('A', money, options) do |xml|
          add_payment_source(xml, creditcard_or_billing_id, options[:currency])
          add_address(xml, creditcard_or_billing_id, options)
          
          # if credit card and billing_id passed in create new customer profile
          assign_customer_ref_num(xml, options[:billing_id]) if options[:billing_id]
        end
        commit(order)
      end
      
      # AC – Authorization and Capture
      def purchase(money, creditcard_or_billing_id, options = {})
        order = build_new_order_xml('AC', money, options) do |xml|
          add_payment_source(xml, creditcard_or_billing_id, options[:currency])
          add_address(xml, creditcard_or_billing_id, options)

          # For new customers
          assign_customer_ref_num(xml, options[:billing_id]) if options[:billing_id]
        end
        commit(order)
      end                       
      
      # MFC - Mark For Capture
      def capture(money, authorization, options = {})
        commit(build_mark_for_capture_xml(money, authorization, options))
      end
      
      # R – Refund request
      def refund(money, authorization, options = {})
        order = build_new_order_xml('R', money, options.merge(:authorization => authorization)) do |xml|
          add_refund(xml, options[:currency])
        end
        commit(order)
      end

      def credit(money, authorization, options= {})
        deprecated CREDIT_DEPRECATION_MESSAGE
        refund(money, authorization, options)
      end
      
      # setting money to nil will perform a full void
      def void(authorization, options = {})
        order = build_void_request_xml(authorization, options)
        commit(order)
      end
      
      # To store a credit card just do a $0 authorization with a billing_id
      def store(creditcard, options = {})
        if options[:billing_id]
          options[:order_id] = options[:billing_id] unless options[:order_id]
          # Authorizing with amount 0 is the same as verifying the card and creating a profile
          authorize(0, creditcard, options)
        end
      end
      
      def unstore(billing_id, options = {})
        parameters = {
          :billing_id => billing_id,
          :action => 'D'
        }
        
        order = build_profile_request_xml(parameters)
        commit(order)
      end
      
      def recurring(money, billing_id, options = {})
        requires!(options, [:periodicity, :monthly, :weekly, :yearly] )
        requires!(options, :start_date)

        # Conform to Orbital's recurring time format
        start_date = Date.new(
          options[:start_date][4..7].to_i, 
          options[:start_date][0..1].to_i,
          options[:start_date][2..3].to_i
        )
        
        cycle = case options[:periodicity]
        when :monthly
         "#{start_date.day} * ?"
        when :weekly
         "? * #{start_date.wday + 1}"
        when :yearly
         "#{start_date.day} #{start_date.month} ?"
        end
        
        parameters = {
          :billing_id => billing_id,
          :action => 'U',
          :money => money,
          :cycle => cycle,
          :customer_ref_num => options[:billingid] || options[:billing_id] || nil,
          :payments => options[:payments] || nil,
          :start_date => options[:start_date] # MMDDYYYY e.g. 05252012 for May 25, 2012
        }

        order = build_profile_request_xml(parameters) do |xml|
          add_recurring(xml, parameters)
        end
        commit(order)
      end
       
      private                       
            
      def add_recurring(xml, options = {})
        xml.tag! :MBType, 'R'
        xml.tag! :MBOrderIdGenerationMethod, 'IO'
        xml.tag! :MBRecurringStartDate, options[:start_date]
        xml.tag! :MBRecurringNoEndDateFlag, 'Y' unless options[:payments]
        xml.tag! :MBRecurringMaxBillings, options[:payments] if options[:payments]
        xml.tag! :MBRecurringFrequency, options[:cycle]
      end
      
      def add_customer_data(xml, customer_ref_num)
        xml.tag! :CustomerRefNum, format_billing_id(customer_ref_num)
      end
      
      def assign_customer_ref_num(xml, customer_ref_num, options = {})
        xml.tag! :CustomerProfileFromOrderInd, 'S'
        add_customer_data(xml, customer_ref_num)
        xml.tag! :CustomerProfileOrderOverrideInd, 'NO'
      end
      
      def add_soft_descriptors(xml, soft_desc)
        xml.tag! :SDMerchantName, soft_desc.merchant_name
        xml.tag! :SDProductDescription, soft_desc.product_description
        xml.tag! :SDMerchantCity, soft_desc.merchant_city
        xml.tag! :SDMerchantPhone, soft_desc.merchant_phone
        xml.tag! :SDMerchantURL, soft_desc.merchant_url
        xml.tag! :SDMerchantEmail, soft_desc.merchant_email
      end

      def add_address(xml, source, options)      
        if address = options[:billing_address] || options[:address]
          xml.tag! :AVSzip, address[:zip]
          xml.tag! :AVSaddress1, address[:address1] if address[:address1]
          xml.tag! :AVSaddress2, address[:address2] if address[:address2]
          xml.tag! :AVScity, address[:city] if address[:city]
          xml.tag! :AVSstate, address[:state] if address[:state]
          xml.tag! :AVSphoneNum, address[:phone] ? address[:phone].scan(/\d/).join.to_s : nil
          xml.tag! :AVScountryCode, address[:country] if address[:country]
          if source.is_a?(String)
            xml.tag! :AVSname, address[:name] if address[:name]
          else
            xml.tag! :AVSname, source.name
          end
        end
      end

      def add_creditcard(xml, creditcard, currency=nil)
        xml.tag! :AccountNum, creditcard.number
        xml.tag! :Exp, expiry_date(creditcard)
        
        xml.tag! :CurrencyCode, currency_code(currency)
        xml.tag! :CurrencyExponent, '2' # Will need updating to support currencies such as the Yen.
        
        xml.tag! :CardSecValInd, 1 if creditcard.verification_value? && %w( visa discover ).include?(creditcard.brand)
        xml.tag! :CardSecVal,  creditcard.verification_value if creditcard.verification_value?
      end
      
      def add_payment_source(xml, source, currency=nil)
        if source.is_a?(String)
          add_customer_data(xml, source)
        else
          add_creditcard(xml, source, currency)
        end
      end
      
      def add_customer_creditcard(xml, creditcard, currency=nil)
        xml.tag! :CCAccountNum, creditcard.number
        xml.tag! :CCExpireDate, expiry_date(creditcard)
      end
      
      def add_refund(xml, currency=nil)
        xml.tag! :AccountNum, nil
        
        xml.tag! :CurrencyCode, currency_code(currency)
        xml.tag! :CurrencyExponent, '2' # Will need updating to support currencies such as the Yen.
      end
      
      def parse(body)
        response = {}
        xml = REXML::Document.new(body)
        root = REXML::XPath.first(xml, "//Response") ||
               REXML::XPath.first(xml, "//ErrorResponse")
        if root
          root.elements.to_a.each do |node|
            recurring_parse_element(response, node)
          end
        end
        response
      end     
      
      def recurring_parse_element(response, node)
        if node.has_elements?
          node.elements.each{|e| recurring_parse_element(response, e) }
        else
          response[node.name.underscore.to_sym] = node.text
        end
      end
      
      def commit(order)
        headers = POST_HEADERS.merge("Content-length" => order.size.to_s)
        request = lambda {return parse(ssl_post(remote_url, order, headers))}
        
        # Failover URL will be used in the event of a connection error
        begin response = request.call; rescue ConnectionError; retry end
        
        Response.new(success?(response), message_from(response), response,
          {:authorization => "#{response[:tx_ref_num]};#{response[:order_id]}",
           :test => self.test?,
           :avs_result => {:code => response[:avs_resp_code]},
           :cvv_result => response[:cvv2_resp_code]
          }
        )
      end
      
      def remote_url
        unless $!.class == ActiveMerchant::ConnectionError
          self.test? ? self.primary_test_url : self.primary_live_url
        else
          self.test? ? self.secondary_test_url : self.secondary_live_url
        end
      end

      def success?(response)
        if response[:message_type].nil? || response[:message_type] == "R"
          response[:proc_status] == SUCCESS
        else
          response[:proc_status] == SUCCESS &&
            response[:resp_code] == APPROVED
        end
      end
      
      def message_from(response)
        success?(response) ? 'APPROVED' : response[:resp_msg] || response[:status_msg]
      end
      
      def ip_authentication?
        @options[:ip_authentication] == true
      end

      def build_new_order_xml(action, money, parameters = {})
        requires!(parameters, :order_id)
        xml = xml_envelope
        xml.tag! :Request do
          xml.tag! :NewOrder do
            add_xml_credentials(xml)
            xml.tag! :IndustryType, parameters[:industry_type] || "EC"
            xml.tag! :MessageType, action
            add_bin_merchant_and_terminal(xml, parameters)
            
            yield xml if block_given?
            
            xml.tag! :Comments, parameters[:comments] if parameters[:comments]
            xml.tag! :OrderID, format_order_id(parameters[:order_id])
            xml.tag! :Amount, amount(money)
            
            # Append Transaction Reference Number at the end for Refund transactions
            if action == "R"
              tx_ref_num, _ = parameters[:authorization].split(';')
              xml.tag! :TxRefNum, tx_ref_num
            end
          end
        end
        xml.target!
      end
      
      def build_mark_for_capture_xml(money, authorization, parameters = {})
        tx_ref_num, order_id = authorization.split(';')
        xml = xml_envelope
        xml.tag! :Request do
          xml.tag! :MarkForCapture do
            add_xml_credentials(xml)
            xml.tag! :OrderID, order_id
            xml.tag! :Amount, amount(money)
            add_bin_merchant_and_terminal(xml, parameters)
            xml.tag! :TxRefNum, tx_ref_num
          end
        end
        xml.target!
      end
      
      def build_void_request_xml(authorization, parameters = {})
        tx_ref_num, order_id = authorization.split(';')
        xml = xml_envelope
        xml.tag! :Request do
          xml.tag! :Reversal do
            add_xml_credentials(xml)
            xml.tag! :TxRefNum, tx_ref_num
            xml.tag! :TxRefIdx, parameters[:transaction_index]
            xml.tag! :OrderID, order_id
            add_bin_merchant_and_terminal(xml, parameters)
          end
        end
        xml.target!
      end
      
      def build_profile_request_xml(parameters = {})
        xml = xml_envelope
        xml.tag! :Request do
          xml.tag! :Profile do
            add_xml_credentials(xml)
            add_customer_bin_merchant_and_terminal(xml, parameters)
            add_customer_data(xml, parameters[:billing_id])
            xml.tag! :CustomerProfileAction, parameters[:action]
            xml.tag! :OrderDefaultAmount, amount(parameters[:money]) if parameters[:money]
            yield xml if block_given?
          end
        end
        xml.target!
      end
      
      def currency_code(currency)
        CURRENCY_CODES[(currency || self.default_currency)].to_s
      end
      
      def expiry_date(credit_card)
        "#{format(credit_card.month, :two_digits)}#{format(credit_card.year, :two_digits)}"
      end

      def bin
        @options[:bin] || (salem_mid? ? '000001' : '000002')
      end

      def xml_envelope
        xml = Builder::XmlMarkup.new(:indent => 2)
        xml.instruct!(:xml, :version => '1.0', :encoding => 'UTF-8')
        xml
      end

      def add_xml_credentials(xml)
        xml.tag! :OrbitalConnectionUsername, @options[:login] unless ip_authentication?
        xml.tag! :OrbitalConnectionPassword, @options[:password] unless ip_authentication?
      end

      def add_bin_merchant_and_terminal(xml, parameters)
        xml.tag! :BIN, bin
        xml.tag! :MerchantID, @options[:merchant_id]
        xml.tag! :TerminalID, parameters[:terminal_id] || '001'
      end
      
      def add_customer_bin_merchant_and_terminal(xml, parameters)
        xml.tag! :CustomerBin, bin
        xml.tag! :CustomerMerchantID, @options[:merchant_id]
        # xml.tag! :CustomerTerminalID, parameters[:terminal_id] || '001'
      end

      def salem_mid?
        @options[:merchant_id].length == 6
      end

      # The valid characters include:
      #
      # 1. all letters and digits
      # 2. - , $ @ & and a space character, though the space character cannot be the leading character
      # 3. PINless Debit transactions can only use uppercase and lowercase alpha (A-Z, a-z) and numeric (0-9)
      def format_order_id(order_id)
        illegal_characters = /[^,$@\- \w]/
        order_id = order_id.to_s.gsub(/\./, '-')
        order_id.gsub!(illegal_characters, '')
        order_id[0...22]
      end
      
      # Billing IDs have the same restrictions as order IDs
      def format_billing_id(billing_id)
        format_order_id(billing_id)
      end
    end
  end
end
{'fraction_non_alphanumeric': 0.10140093395597065, 'fraction_numerical': 0.00790527018012008, 'mean_word_length': 2.2899155053220674, 'pattern_counts': {'":': 0, '<': 6, '<?xml version=': 0, '>': 77, 'https://': 8, 'lorem ipsum': 0, 'www.': 0, 'xml': 241}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7479403', 'n_tokens_mistral': 9113, 'n_tokens_neox': 8773, 'n_words': 2116}