henkm/docdata

View on GitHub
lib/docdata/payment.rb

Summary

Maintainability
A
2 hrs
Test Coverage
module Docdata

  # Creates a validator
  class PaymentValidator
    include Veto.validator
    validates :amount, presence: true, integer: true
    validates :profile, presence: true
    validates :currency, presence: true, format: /[A-Z]{3}/
    validates :order_reference, presence: true
  end



  #
  # Object representing a "WSDL" object with attributes provided by Docdata
  #
  # @example
  #   Payment.new({
  #     :amount => 2500,
  #     :currency => "EUR",
  #     :order_reference => "TJ123"
  #     :profile => "MyProfile"
  #     :shopper => @shopper
  #   })
  # 
  # @return [Array] Errors
  # @param :amount [Integer] The total price in cents
  # @param :currency [String] ISO currency code (USD, EUR, GBP, etc.)
  # @param :order_reference [String] A unique order reference
  # @param :profile [String] The DocData payment profile (e.g. 'MyProfile')
  # @param :description [String] Description for this payment
  # @param :receipt_text [String] A receipt text
  # @param :shopper [Docdata::Shopper] A shopper object (instance of Docdata::Shopper)
  # @param :bank_id [String] (optional) in case you want to redirect the consumer
  # directly to the bank page (iDeal), you can set the bank id ('0031' for ABN AMRO for example.)
  # @param :prefered_payment_method [String] (optional) set a prefered payment method.
  # any of: [IDEAL, AMAX, VISA, etc.]
  # @param :line_items [Array] (optional) Array of objects of type Docdata::LineItem
  # @param :default_act [Boolean] (optional) Should the redirect URL contain a default_act=true parameter?
  # 
  class Payment
    attr_accessor :errors
    attr_accessor :amount
    @@amount = "?"
    attr_accessor :description
    attr_accessor :receipt_text
    attr_accessor :currency
    attr_accessor :order_reference
    attr_accessor :profile
    attr_accessor :shopper
    attr_accessor :bank_id
    attr_accessor :prefered_payment_method
    attr_accessor :line_items
    attr_accessor :key
    attr_accessor :default_act
    attr_accessor :canceled
    attr_accessor :id


    #
    # Initializer to transform a +Hash+ into an Payment object
    #
    # @param [Hash] args
    def initialize(args=nil)
      @line_items = []
      return if args.nil?
      args.each do |k,v|
        instance_variable_set("@#{k}", v) unless v.nil?
      end
    end


    # @return [String] a cleaned up version of the description string
    # where forbidden characters are filtered out and limit is 50 chars.
    def cleaned_up_description
      description.gsub("&", "and")[0..49]
    end

    # @return [Boolean] true/false, depending if this instanciated object is valid
    def valid?
      validator = PaymentValidator.new
      validator.valid?(self)
    end

    # 
    # This is the most importent method. It uses all the attributes
    # and performs a `create` action on Docdata Payments SOAP API. 
    # @return [Docdata::Response] response object with `key`, `message` and `success?` methods
    # 
    def create
      # if there are any line items, they should all be valid.
      validate_line_items

      # make the SOAP API call
      response        = Docdata.client.call(:create, xml: create_xml)
      response_object = Docdata::Response.parse(:create, response)
      if response_object.success?
        self.key = response_object.key
      end

      # set `self` as the value of the `payment` attribute in the response object
      response_object.payment = self
      response_object.url     = redirect_url

      return response_object
    end

    # 
    # This calls the 'cancel' method of the SOAP API
    # It cancels the payment and returns a Docdata::Response object
    def cancel
      # make the SOAP API call
      response        = Docdata.client.call(:cancel, xml: cancel_xml)
      response_object = Docdata::Response.parse(:cancel, response)
      if response_object.success?
        self.key = response_object.key
      end

      # set `self` as the value of the `payment` attribute in the response object
      response_object.payment = self
      self.canceled = true
      return true
    end

    # 
    # This calls the 'refund' method of the SOAP API
    # It refunds (part of) the amount paid
    def refund(amount_to_refund, refund_description="")
      p = Docdata::Payment.new(key: key)
      p = p.status.payment
      refund_object = Docdata::Refund.new(
        currency: p.currency,
        amount: amount_to_refund,
        description: refund_description,
        payment: p
      )
      if refund_object.valid?
        refund_object.perform_refund
      else
        raise DocdataError.new(refund_object), refund_object.errors.full_messages.join(", ")
      end
    end



    # This method makes it possible to find and cancel a payment with only the key
    # It combines 
    def self.cancel(api_key)
      p = self.find(api_key)
      p.cancel
    end

    # This method makes it possible to find and refund a payment with only the key
    # exmaple usage: Docdata::Payment.refund("APIT0K3N", 250)
    def self.refund(api_key, amount_to_refund, refund_description="")
      p = self.find(api_key)
      p.refund(amount_to_refund, refund_description)
    end


    # Initialize a Payment object with the key set
    def self.find(api_key)
      p = self.new(key: api_key)
      if p.status.success
        return p
      else
        raise DocdataError.new(p), p.status.message
      end
    end

    # 
    # This is one of the other native SOAP API methods.
    # @return [Docdata::Response]
    def status
      # read the xml template
      xml_file        = "#{File.dirname(__FILE__)}/xml/status.xml.erb"
      template        = File.read(xml_file)      
      namespace       = OpenStruct.new(payment: self)
      xml             = ERB.new(template).result(namespace.instance_eval { binding })

      # puts xml

      response        = Docdata.client.call(:status, xml: xml)
      response_object = Docdata::Response.parse(:status, response)

      response_object.set_attributes

      self.id                 = response_object.pid
      self.currency           = response_object.currency
      response_object.key     = key
      response_object.payment = self
      return response_object # Docdata::Response
    end
    alias_method :check, :status

    # @return [String] The URI where the consumer can be redirected to in order to pay
    def redirect_url
      url = {}
      
      base_url = Docdata::Config.return_url
      if Docdata::Config.test_mode
        redirect_base_url = 'https://test.docdatapayments.com/ps/menu'
      else
        redirect_base_url = 'https://secure.docdatapayments.com/ps/menu'
      end
      url[:command]             = "show_payment_cluster"
      url[:payment_cluster_key] = key
      url[:merchant_name]       = Docdata::Config.username
      # only include return URL if present
      if base_url.present?
        url[:return_url_success]  = "#{base_url}/success?key=#{url[:payment_cluster_key]}"
        url[:return_url_pending]  = "#{base_url}/pending?key=#{url[:payment_cluster_key]}"
        url[:return_url_canceled] = "#{base_url}/canceled?key=#{url[:payment_cluster_key]}"
        url[:return_url_error]    = "#{base_url}/error?key=#{url[:payment_cluster_key]}"
      end
      if shopper && shopper.language_code
        url[:client_language]      = shopper.language_code
      end
      if default_act
        url[:default_act]     = "yes"
      end
      if bank_id.present?
        url[:ideal_issuer_id] = bank_id
        url[:default_pm]      = "IDEAL"
      end
      if prefered_payment_method.present?
        url[:default_pm]      = prefered_payment_method
      end
      params = URI.encode_www_form(url)
      uri = "#{redirect_base_url}?#{params}"
    end
    alias_method :url, :redirect_url


    # @return [String] the xml to send in the SOAP API
    def create_xml
      xml_file        = "#{File.dirname(__FILE__)}/xml/create.xml.erb"
      template        = File.read(xml_file)      
      namespace       = OpenStruct.new(payment: self, shopper: shopper)
      xml             = ERB.new(template).result(namespace.instance_eval { binding })
    end


    # @return [String] the xml to send in the SOAP API
    def cancel_xml
      xml_file        = "#{File.dirname(__FILE__)}/xml/cancel.xml.erb"
      template        = File.read(xml_file)      
      namespace       = OpenStruct.new(payment: self)
      xml             = ERB.new(template).result(namespace.instance_eval { binding })
    end


    private

    # In case there are any line_items, validate them all and
    # raise an error for the first invalid LineItem
    def validate_line_items
      if @line_items.any?
        for line_item in @line_items
          if line_item.valid?
            # do nothing, this line_item seems okay
          else
            raise DocdataError.new(line_item), line_item.error_message
          end
        end
      end
    end

    # @return [Hash] list of VAT-rates and there respective totals
    def vat_rates
      rates = {}
      for item in @line_items
        rates["vat_#{item.vat_rate.to_s}"] ||= {}
        rates["vat_#{item.vat_rate.to_s}"][:rate] ||= item.vat_rate
        rates["vat_#{item.vat_rate.to_s}"][:total] ||= 0
        rates["vat_#{item.vat_rate.to_s}"][:total] += item.vat
      end
      return rates
    end

  end
end