require 'rexml/document'

module ActiveMerchant #:nodoc:
  module Billing #:nodoc:
    
    # Initialization Options
    # :login                Your store number
    # :pem                  The text of your linkpoint PEM file. Note
    #                       this is not the path to file, but its
    #                       contents. If you are only using one PEM
    #                       file on your site you can declare it 
    #                       globally and then you won't need to
    #                       include this option
    #
    #
    # A valid store number is required. Unfortunately, with LinkPoint 
    # YOU CAN'T JUST USE ANY OLD STORE NUMBER. Also, you can't just 
    # generate your own PEM file. You'll need to use a special PEM file 
    # provided by LinkPoint. 
    #
    # Go to http://www.linkpoint.com/support/sup_teststore.asp to set up 
    # a test account and obtain your PEM file.
    #
    # Declaring PEM file Globally
    # ActiveMerchant::Billing::LinkpointGateway.pem_file = File.read( File.dirname(__FILE__) + '/../mycert.pem' )
    # 
    # 
    # Valid Order Options
    # :result => 
    #   LIVE                  Production mode
    #   GOOD                  Approved response in test mode
    #   DECLINE               Declined response in test mode
    #   DUPLICATE             Duplicate response in test mode
    #                     
    # :ponumber               Order number
    #
    # :transactionorigin =>   Source of the transaction
    #    ECI                  Email or Internet
    #    MAIL                 Mail order
    #    MOTO                 Mail order/Telephone
    #    TELEPHONE            Telephone
    #    RETAIL               Face-to-face
    #
    # :ordertype =>       
    #    SALE                 Real live sale
    #    PREAUTH              Authorize only
    #    POSTAUTH             Forced Ticket or Ticket Only transaction
    #    VOID             
    #    CREDIT           
    #    CALCSHIPPING         For shipping charges calculations
    #    CALCTAX              For sales tax calculations
    #                     
    # Recurring Options   
    # :action =>          
    #    SUBMIT           
    #    MODIFY           
    #    CANCEL           
    #                     
    # :installments           Identifies how many recurring payments to charge the customer
    # :startdate              Date to begin charging the recurring payments. Format: YYYYMMDD or "immediate"
    # :periodicity  =>    
    #     MONTHLY         
    #     BIMONTHLY       
    #     WEEKLY          
    #     BIWEEKLY        
    #     YEARLY          
    #     DAILY           
    # :threshold              Tells how many times to retry the transaction (if it fails) before contacting the merchant.
    # :comments               Uh... comments
    #
    #
    # For reference: 
    #
    # https://www.linkpointcentral.com/lpc/docs/Help/APIHelp/lpintguide.htm
    #
    #  Entities = {
    #    :payment => [:subtotal, :tax, :vattax, :shipping, :chargetotal],
    #    :billing => [:name, :address1, :address2, :city, :state, :zip, :country, :email, :phone, :fax, :addrnum],
    #    :shipping => [:name, :address1, :address2, :city, :state, :zip, :country, :weight, :items, :carrier, :total],
    #    :creditcard => [:cardnumber, :cardexpmonth, :cardexpyear, :cvmvalue, :track],
    #    :telecheck => [:routing, :account, :checknumber, :bankname, :bankstate, :dl, :dlstate, :void, :accounttype, :ssn],
    #    :transactiondetails => [:transactionorigin, :oid, :ponumber, :taxexempt, :terminaltype, :ip, :reference_number, :recurring, :tdate],
    #    :periodic => [:action, :installments, :threshold, :startdate, :periodicity, :comments],
    #    :notes => [:comments, :referred]
    #    :items => [:item => [:price, :quantity, :description, :id, :options => [:option => [:name, :value]]]]
    #  }
    #
    # 
    # LinkPoint's Items entity is an optional entity that can be attached to orders.
    # It is entered as :line_items to be consistent with the CyberSource implementation
    #
    # The line_item hash goes in the options hash and should look like
    #
    #         :line_items => [
    #           {
    #             :id => '123456',
    #             :description => 'Logo T-Shirt',
    #             :price => '12.00',
    #             :quantity => '1',
    #             :options => [
    #               {
    #                 :name => 'Color',
    #                 :value => 'Red'
    #               },
    #               {
    #                 :name => 'Size',
    #                 :value => 'XL'
    #               }
    #             ]
    #           },
    #           {
    #             :id => '111',
    #             :description => 'keychain',
    #             :price => '3.00',
    #             :quantity => '1'
    #           }
    #         ]
    # This functionality is only supported by this particular gateway may
    # be changed at any time
    # 
    class LinkpointGateway < Gateway     
      # Your global PEM file. This will be assigned to you by linkpoint
      # 
      # Example: 
      # 
      # ActiveMerchant::Billing::LinkpointGateway.pem_file = File.read( File.dirname(__FILE__) + '/../mycert.pem' )
      # 
      cattr_accessor :pem_file
      
      self.test_url  = 'https://staging.linkpt.net:1129/'
      self.live_url  = 'https://secure.linkpt.net:1129/'
      
      self.supported_countries = ['US']
      self.supported_cardtypes = [:visa, :master, :american_express, :discover, :jcb, :diners_club]
      self.homepage_url = 'http://www.linkpoint.com/'
      self.display_name = 'LinkPoint'
           
      def initialize(options = {})
        requires!(options, :login)
        
        @options = {
          :result => 'LIVE',
          :pem => LinkpointGateway.pem_file
        }.update(options)
        
        raise ArgumentError, "You need to pass in your pem file using the :pem parameter or set it globally using ActiveMerchant::Billing::LinkpointGateway.pem_file = File.read( File.dirname(__FILE__) + '/../mycert.pem' ) or similar" if @options[:pem].blank?
      end
      
      # Send a purchase request with periodic options
      # Recurring Options   
      # :action =>          
      #    SUBMIT           
      #    MODIFY           
      #    CANCEL           
      #                     
      # :installments           Identifies how many recurring payments to charge the customer
      # :startdate              Date to begin charging the recurring payments. Format: YYYYMMDD or "immediate"
      # :periodicity  =>    
      #     :monthly         
      #     :bimonthly       
      #     :weekly          
      #     :biweekly        
      #     :yearly          
      #     :daily           
      # :threshold              Tells how many times to retry the transaction (if it fails) before contacting the merchant.
      # :comments               Uh... comments
      #
      def recurring(money, creditcard, options={})
        requires!(options, [:periodicity, :bimonthly, :monthly, :biweekly, :weekly, :yearly, :daily], :installments, :order_id )
        
        options.update(
          :ordertype => "SALE",
          :action => options[:action] || "SUBMIT",
          :installments => options[:installments] || 12,
          :startdate => options[:startdate] || "immediate",
          :periodicity => options[:periodicity].to_s || "monthly",
          :comments => options[:comments] || nil,
          :threshold => options[:threshold] || 3
        )
        commit(money, creditcard, options)
      end
      
      # Buy the thing
      def purchase(money, creditcard, options={})
        requires!(options, :order_id)
        options.update(
          :ordertype => "SALE"
        )
        commit(money, creditcard, options)
      end
      
      #
      # Authorize the transaction
      # 
      # Reserves the funds on the customer's credit card, but does not charge the card.
      #
      def authorize(money, creditcard, options = {})
        requires!(options, :order_id)
        options.update(
          :ordertype => "PREAUTH"
        )
        commit(money, creditcard, options)
      end
      
      #
      # Post an authorization. 
      #
      # Captures the funds from an authorized transaction. 
      # Order_id must be a valid order id from a prior authorized transaction.
      # 
      def capture(money, authorization, options = {})
        options.update(
          :order_id => authorization,
          :ordertype => "POSTAUTH"
        )
        commit(money, nil, options)  
      end
      
      # Void a previous transaction
      def void(identification, options = {})
        options.update(
          :order_id => identification,
          :ordertype => "VOID"
        )
        commit(nil, nil, options)
      end
      
      # 
      # Refund an order
      # 
      # identification must be a valid order id previously submitted by SALE
      #
      def refund(money, identification, options = {})
        options.update(
          :ordertype => "CREDIT",
          :order_id => identification
        )
        commit(money, nil, options)
      end

      def credit(money, identification, options = {})
        deprecated CREDIT_DEPRECATION_MESSAGE
        refund(money, identification, options)
      end
    
      def test?
        @options[:test] || super
      end
      
      private
      # Commit the transaction by posting the XML file to the LinkPoint server
      def commit(money, creditcard, options = {})
        response = parse(ssl_post(test? ? self.test_url : self.live_url, post_data(money, creditcard, options)))
        
        Response.new(successful?(response), response[:message], response, 
          :test => test?,
          :authorization => response[:ordernum],
          :avs_result => { :code => response[:avs].to_s[2,1] },
          :cvv_result => response[:avs].to_s[3,1]
        )
      end
      
      def successful?(response)
        response[:approved] == "APPROVED"
      end
      
      # Build the XML file
      def post_data(money, creditcard, options)
        params = parameters(money, creditcard, options)
        
        xml = REXML::Document.new
        order = xml.add_element("order")
        
        # Merchant Info
        merchantinfo = order.add_element("merchantinfo")
        merchantinfo.add_element("configfile").text = @options[:login]
        
        # Loop over the params hash to construct the XML string
        for key, value in params
          elem = order.add_element(key.to_s)
          if key == :items
            build_items(elem, value)
          else
            for k, v in params[key]
              elem.add_element(k.to_s).text = params[key][k].to_s if params[key][k]
            end
          end
          # Linkpoint doesn't understand empty elements: 
          order.delete(elem) if elem.size == 0
        end
        return xml.to_s
      end

      # adds LinkPoint's Items entity to the XML.  Called from post_data
      def build_items(element, items)
        for item in items
          item_element = element.add_element("item")
          for key, value in item
            if key == :options
              options_element = item_element.add_element("options")
              for option in value
                opt_element = options_element.add_element("option")
                opt_element.add_element("name").text =  option[:name]   unless option[:name].blank?
                opt_element.add_element("value").text =  option[:value]   unless option[:value].blank?
              end
            else
              item_element.add_element(key.to_s).text =  item[key].to_s unless item[key].blank?
            end
          end
        end
      end

      # Set up the parameters hash just once so we don't have to do it
      # for every action. 
      def parameters(money, creditcard, options = {})
        
        params = {
          :payment => {
            :subtotal => amount(options[:subtotal]),
            :tax => amount(options[:tax]),
            :vattax => amount(options[:vattax]),
            :shipping => amount(options[:shipping]),
            :chargetotal => amount(money)
          },
          :transactiondetails => {
            :transactionorigin => options[:transactionorigin] || "ECI",
            :oid => options[:order_id],
            :ponumber => options[:ponumber],
            :taxexempt => options[:taxexempt],
            :terminaltype => options[:terminaltype],
            :ip => options[:ip],
            :reference_number => options[:reference_number],
            :recurring => options[:recurring] || "NO",  #DO NOT USE if you are using the periodic billing option. 
            :tdate => options[:tdate]
          },
          :orderoptions => {
            :ordertype => options[:ordertype],
            :result => @options[:result]
          },
          :periodic => {
            :action => options[:action],
            :installments => options[:installments], 
            :threshold => options[:threshold], 
            :startdate => options[:startdate], 
            :periodicity => options[:periodicity], 
            :comments => options[:comments]
          },
          :telecheck => {
            :routing => options[:telecheck_routing],
            :account => options[:telecheck_account],
            :checknumber => options[:telecheck_checknumber],
            :bankname => options[:telecheck_bankname],
            :dl => options[:telecheck_dl],
            :dlstate => options[:telecheck_dlstate],
            :void => options[:telecheck_void],
            :accounttype => options[:telecheck_accounttype],
            :ssn => options[:telecheck_ssn],
          }
        }
      
        if creditcard
          params[:creditcard] = {
            :cardnumber => creditcard.number,
            :cardexpmonth => creditcard.month,
            :cardexpyear => format_creditcard_expiry_year(creditcard.year),
            :track => nil
          }
          
          if creditcard.verification_value?
            params[:creditcard][:cvmvalue] = creditcard.verification_value
            params[:creditcard][:cvmindicator] = 'provided'
          else
            params[:creditcard][:cvmindicator] = 'not_provided'
          end          
        end
        
        if billing_address = options[:billing_address] || options[:address]          
          
          params[:billing] = {}        
          params[:billing][:name]      = billing_address[:name] || (creditcard ? creditcard.name : nil)
          params[:billing][:address1]  = billing_address[:address1] unless billing_address[:address1].blank?
          params[:billing][:address2]  = billing_address[:address2] unless billing_address[:address2].blank?
          params[:billing][:city]      = billing_address[:city]     unless billing_address[:city].blank?
          params[:billing][:state]     = billing_address[:state]    unless billing_address[:state].blank?
          params[:billing][:zip]       = billing_address[:zip]      unless billing_address[:zip].blank?
          params[:billing][:country]   = billing_address[:country]  unless billing_address[:country].blank?
          params[:billing][:company]   = billing_address[:company]  unless billing_address[:company].blank?
          params[:billing][:phone]     = billing_address[:phone]  unless billing_address[:phone].blank?
          params[:billing][:email]     = options[:email] unless options[:email].blank?
        end                

        if shipping_address = options[:shipping_address] 

          params[:shipping] = {}
          params[:shipping][:name]      = shipping_address[:name] || creditcard ? creditcard.name : nil
          params[:shipping][:address1]  = shipping_address[:address1] unless shipping_address[:address1].blank?
          params[:shipping][:address2]  = shipping_address[:address2] unless shipping_address[:address2].blank?
          params[:shipping][:city]      = shipping_address[:city]     unless shipping_address[:city].blank?
          params[:shipping][:state]     = shipping_address[:state]    unless shipping_address[:state].blank?
          params[:shipping][:zip]       = shipping_address[:zip]      unless shipping_address[:zip].blank?
          params[:shipping][:country]   = shipping_address[:country]  unless shipping_address[:country].blank?
        end        

        params[:items] = options[:line_items] if options[:line_items]
        
        return params
      end
        
      def parse(xml)
        
        # For reference, a typical response...
        # <r_csp></r_csp>
        # <r_time></r_time>
        # <r_ref></r_ref>
        # <r_error></r_error>
        # <r_ordernum></r_ordernum>
        # <r_message>This is a test transaction and will not show up in the Reports</r_message>
        # <r_code></r_code>
        # <r_tdate>Thu Feb 2 15:40:21 2006</r_tdate>
        # <r_score></r_score>
        # <r_authresponse></r_authresponse>
        # <r_approved>APPROVED</r_approved>
        # <r_avs></r_avs>
        
        response = {:message => "Global Error Receipt", :complete => false}
        
        xml = REXML::Document.new("<response>#{xml}</response>")
        xml.root.elements.each do |node|
          response[node.name.downcase.sub(/^r_/, '').to_sym] = normalize(node.text)
        end unless xml.root.nil?
        
        response
      end
    
      # Make a ruby type out of the response string
      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 format_creditcard_expiry_year(year)
        sprintf("%.4i", year)[-2..-1]
      end      
    end
  end
end
