# encoding: utf-8
require 'cgi'
require 'openssl'
require 'base64'
require 'rexml/document'
module Ideal
# The base class for all iDEAL response classes.
#
# Note that if the iDEAL system is under load it will _not_ allow more
# then two retries per request.
class Response
attr_accessor :response
def initialize(response_body, options = {})
@response = REXML::Document.new(response_body).root
@success = !error_occured?
@test = options[:test]
end
# Returns whether we're running in test mode
def test?
@test
end
# Returns whether the request was a success
def success?
@success
end
# Returns a technical error message.
def error_message
text('//Error/errorMessage') unless success?
end
# Returns a consumer friendly error message.
def consumer_error_message
text('//Error/consumerMessage') unless success?
end
# Returns details on the error if available.
def error_details
text('//Error/errorDetail') unless success?
end
# Returns an error type inflected from the first two characters of the
# error code. See error_code for a full list of errors.
#
# Error code to type mappings:
#
# * +IX+ - :xml
# * +SO+ - :system
# * +SE+ - :security
# * +BR+ - :value
# * +AP+ - :application
def error_type
unless success?
case error_code[0,2]
when 'IX' then :xml
when 'SO' then :system
when 'SE' then :security
when 'BR' then :value
when 'AP' then :application
end
end
end
# Returns the code of the error that occured.
#
# === Codes
#
# ==== IX: Invalid XML and all related problems
#
# Such as incorrect encoding, invalid version, or otherwise unreadable:
#
# * IX1000 - Received XML not well-formed.
# * IX1100 - Received XML not valid.
# * IX1200 - Encoding type not UTF-8.
# * IX1300 - XML version number invalid.
# * IX1400 - Unknown message.
# * IX1500 - Mandatory main value missing. (Merchant ID ?)
# * IX1600 - Mandatory value missing.
#
# ==== SO: System maintenance or failure
#
# The errors that are communicated in the event of system maintenance or
# system failure. Also covers the situation where new requests are no
# longer being accepted but requests already submitted will be dealt with
# (until a certain time):
#
# * SO1000 - Failure in system.
# * SO1200 - System busy. Try again later.
# * SO1400 - Unavailable due to maintenance.
#
# ==== SE: Security and authentication errors
#
# Incorrect authentication methods and expired certificates:
#
# * SE2000 - Authentication error.
# * SE2100 - Authentication method not supported.
# * SE2700 - Invalid electronic signature.
#
# ==== BR: Field errors
#
# Extra information on incorrect fields:
#
# * BR1200 - iDEAL version number invalid.
# * BR1210 - Value contains non-permitted character.
# * BR1220 - Value too long.
# * BR1230 - Value too short.
# * BR1240 - Value too high.
# * BR1250 - Value too low.
# * BR1250 - Unknown entry in list.
# * BR1270 - Invalid date/time.
# * BR1280 - Invalid URL.
#
# ==== AP: Application errors
#
# Errors relating to IDs, account numbers, time zones, transactions:
#
# * AP1000 - Acquirer ID unknown.
# * AP1100 - Merchant ID unknown.
# * AP1200 - Issuer ID unknown.
# * AP1300 - Sub ID unknown.
# * AP1500 - Merchant ID not active.
# * AP2600 - Transaction does not exist.
# * AP2620 - Transaction already submitted.
# * AP2700 - Bank account number not 11-proof.
# * AP2900 - Selected currency not supported.
# * AP2910 - Maximum amount exceeded. (Detailed record states the maximum amount).
# * AP2915 - Amount too low. (Detailed record states the minimum amount).
# * AP2920 - Please adjust expiration period. See suggested expiration period.
def error_code
text('//errorCode') unless success?
end
private
def error_occured?
@response.name == 'ErrorRes'
end
def text(path)
@response.get_text(path).to_s
end
end
# An instance of TransactionResponse is returned from
# Gateway#setup_purchase which returns the service_url to where the
# user should be redirected to perform the transaction _and_ the
# transaction ID.
class TransactionResponse < Response
# Returns the URL to the issuer’s page where the consumer should be
# redirected to in order to perform the payment.
def service_url
CGI::unescapeHTML(text('//issuerAuthenticationURL'))
end
# Returns the transaction ID which is needed for requesting the status
# of a transaction. See Gateway#capture.
def transaction_id
text('//transactionID')
end
# Returns the :order_id for this transaction.
def order_id
text('//purchaseID')
end
end
# An instance of StatusResponse is returned from Gateway#capture
# which returns whether or not the transaction that was started with
# Gateway#setup_purchase was successful.
#
# It takes care of checking if the message was authentic by verifying the
# the message and its signature against the iDEAL certificate.
#
# If success? returns +false+ because the authenticity wasn't verified
# there will be no error_code, error_message, and error_type. Use verified?
# to check if the authenticity has been verified.
class StatusResponse < Response
def initialize(response_body, options = {})
super
@success = transaction_successful?
end
# Returns the status message, which is one of: :success,
# :cancelled, :expired, :open, or
# :failure.
def status
status = text('//status')
status.downcase.to_sym unless (status.strip == '')
end
# Returns whether or not the authenticity of the message could be
# verified.
def verified?
@verified ||= Ideal::Gateway.ideal_certificate.public_key.
verify(OpenSSL::Digest::SHA1.new, signature, message)
end
# Returns the bankaccount number when the transaction was successful.
def consumer_account_number
text('//consumerAccountNumber')
end
# Returns the name on the bankaccount of the customer when the
# transaction was successful.
def consumer_name
text('//consumerName')
end
# Returns the city on the bankaccount of the customer when the
# transaction was successful.
def consumer_city
text('//consumerCity')
end
private
# Checks if no errors occured _and_ if the message was authentic.
def transaction_successful?
!error_occured? && status == :success && verified?
end
# The message that we need to verify the authenticity.
def message
text('//createDateTimeStamp') + text('//transactionID') + text('//status') + text('//consumerAccountNumber')
end
def signature
Base64.decode64(text('//signatureValue'))
end
end
# An instance of DirectoryResponse is returned from
# Gateway#issuers which returns the list of issuers available at the
# acquirer.
class DirectoryResponse < Response
# Returns a list of issuers available at the acquirer.
#
# gateway.issuers.list # => [{ :id => '1006', :name => 'ABN AMRO Bank' }]
def list
@response.get_elements('//Issuer').map do |issuer|
{ :id => issuer.get_text('issuerID').to_s, :name => issuer.get_text('issuerName').to_s }
end
end
end
end