lib/docdata/payment.rb
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