lib/adyen/api.rb in fingertips-adyen-0.3.8.20100924 vs lib/adyen/api.rb in fingertips-adyen-0.3.8.20100929

- old
+ new

@@ -52,11 +52,11 @@ def initialize(params = {}) @params = API.default_params.merge(params) end - def call_webservice_action(action, data) + def call_webservice_action(action, data, response_class) endpoint = self.class.endpoint post = Net::HTTP::Post.new(endpoint.path, 'Accept' => 'text/xml', 'Content-Type' => 'text/xml; charset=utf-8', 'SOAPAction' => action) post.basic_auth(API.username, API.password) post.body = data @@ -65,18 +65,119 @@ request.use_ssl = true request.ca_file = CACERT request.verify_mode = OpenSSL::SSL::VERIFY_PEER request.start do |http| - response = http.request(post) - # TODO: handle not 2xx responses - #p response - XMLQuerier.new(response.body) + response_class.new(http.request(post)) end end end + class Response + def self.response_attrs(*attrs) + attrs.each do |attr| + define_method(attr) { params[attr] } + end + end + + attr_reader :http_response + + def initialize(http_response) + @http_response = http_response + end + + # @return [Boolean] Whether or not the request was successful. + def success? + !http_failure? + end + + # @return [Boolean] Whether or not the HTTP request was a success. + def http_failure? + !@http_response.is_a?(Net::HTTPSuccess) + end + + def xml_querier + @xml_querier ||= XMLQuerier.new(@http_response.body) + end + + def params + raise "The Adyen::API::Response#params method should be overriden in a subclass." + end + end + + class XMLQuerier + NS = { + 'soap' => 'http://schemas.xmlsoap.org/soap/envelope/', + 'payment' => 'http://payment.services.adyen.com', + 'recurring' => 'http://recurring.services.adyen.com', + 'common' => 'http://common.services.adyen.com' + } + + class << self + attr_accessor :backend + + def backend=(backend) + @backend = backend + class_eval do + private + if backend == :nokogiri + def document_for_xml(xml) + Nokogiri::XML::Document.parse(xml) + end + def perform_xpath(query) + @node.xpath(query, NS) + end + else + def document_for_xml(xml) + REXML::Document.new(xml) + end + def perform_xpath(query) + REXML::XPath.match(@node, query, NS) + end + end + end + end + end + + begin + require 'nokogiri' + self.backend = :nokogiri + rescue LoadError + require 'rexml/document' + self.backend = :rexml + end + + def initialize(data) + @node = data.is_a?(String) ? document_for_xml(data) : data + end + + def xpath(query) + result = self.class.new(perform_xpath(query)) + block_given? ? yield(result) : result + end + + def text(query) + xpath("#{query}/text()").to_s.strip + end + + def children + @node.first.children + end + + def empty? + @node.empty? + end + + def to_s + @node.to_s + end + + def map(&block) + @node.map { |n| self.class.new(n) }.map(&block) + end + end + class PaymentService < SimpleSOAPClient ENDPOINT_URI = 'https://pal-%s.adyen.com/pal/servlet/soap/Payment' def authorise_payment make_payment_request(authorise_payment_request_body) @@ -87,19 +188,11 @@ end private def make_payment_request(data) - response = call_webservice_action('authorise', data) - response.xpath('//payment:authoriseResponse/payment:paymentResult') do |result| - { - :psp_reference => result.text('./payment:pspReference'), - :result_code => result.text('./payment:resultCode'), - :auth_code => result.text('./payment:authCode'), - :refusal_reason => result.text('./payment:refusalReason') - } - end + call_webservice_action('authorise', data, AuthorizationResponse) end def authorise_payment_request_body content = card_partial content << RECURRING_PARTIAL if @params[:recurring] @@ -128,31 +221,75 @@ end def shopper_partial @params[:shopper].map { |k, v| SHOPPER_PARTIALS[k] % v }.join("\n") end + + class AuthorizationResponse < Response + ERRORS = { + "validation 101 Invalid card number" => [:number, 'is not a valid creditcard number'], + "validation 103 CVC is not the right length" => [:cvc, 'is not the right length'], + "validation 128 Card Holder Missing" => [:holder_name, 'can’t be blank'], + "validation Couldn't parse expiry year" => [:expiry_year, 'could not be recognized'], + "validation Expiry month should be between 1 and 12 inclusive" => [:expiry_month, 'could not be recognized'], + } + + AUTHORISED = 'Authorised' + + def self.original_fault_message_for(attribute, message) + if error = ERRORS.find { |_, (a, m)| a == attribute && m == message } + error.first + else + message + end + end + + response_attrs :result_code, :auth_code, :refusal_reason, :psp_reference + + def success? + super && params[:result_code] == AUTHORISED + end + + alias authorized? success? + + def invalid_request? + !fault_message.nil? + end + + def error + ERRORS[fault_message] || [:base, fault_message] + end + + def params + @params ||= xml_querier.xpath('//payment:authoriseResponse/payment:paymentResult') do |result| + { + :psp_reference => result.text('./payment:pspReference'), + :result_code => result.text('./payment:resultCode'), + :auth_code => result.text('./payment:authCode'), + :refusal_reason => result.text('./payment:refusalReason') + } + end + end + + private + + def fault_message + @fault_message ||= xml_querier.text('//soap:Fault/faultstring') + end + end end class RecurringService < SimpleSOAPClient ENDPOINT_URI = 'https://pal-%s.adyen.com/pal/servlet/soap/Recurring' # TODO: rename to list_details and make shortcut method take the only necessary param def list - response = call_webservice_action('listRecurringDetails', list_request_body) - response.xpath('//recurring:listRecurringDetailsResponse/recurring:result') do |result| - { - :creation_date => DateTime.parse(result.text('./recurring:creationDate')), - :details => result.xpath('.//recurring:RecurringDetail').map { |node| parse_recurring_detail(node) }, - :last_known_shopper_email => result.text('./recurring:lastKnownShopperEmail'), - :shopper_reference => result.text('./recurring:shopperReference') - } - end + call_webservice_action('listRecurringDetails', list_request_body, ListResponse) end def disable - response = call_webservice_action('disable', disable_request_body) - { :response => response.text('//recurring:disableResponse/recurring:result/recurring:response') } + call_webservice_action('disable', disable_request_body, DisableResponse) end private def list_request_body @@ -164,118 +301,72 @@ reference = RECURRING_DETAIL_PARTIAL % reference end DISABLE_LAYOUT % [@params[:merchant_account], @params[:shopper][:reference], reference || ''] end - # @todo add support for elv - def parse_recurring_detail(node) - result = { - :recurring_detail_reference => node.text('./recurring:recurringDetailReference'), - :variant => node.text('./recurring:variant'), - :creation_date => DateTime.parse(node.text('./recurring:creationDate')) - } + class DisableResponse < Response + response_attrs :response - card = node.xpath('./recurring:card') - if card.children.empty? - result[:bank] = parse_bank_details(node.xpath('./recurring:bank')) - else - result[:card] = parse_card_details(card) + def params + @params ||= { :response => xml_querier.text('//recurring:disableResponse/recurring:result/recurring:response') } end - - result end - def parse_card_details(card) - { - :expiry_date => Date.new(card.text('./payment:expiryYear').to_i, card.text('./payment:expiryMonth').to_i, -1), - :holder_name => card.text('./payment:holderName'), - :number => card.text('./payment:number') - } - end + class ListResponse < Response + response_attrs :details, :last_known_shopper_email, :shopper_reference, :creation_date - def parse_bank_details(bank) - { - :bank_account_number => bank.text('./payment:bankAccountNumber'), - :bank_location_id => bank.text('./payment:bankLocationId'), - :bank_name => bank.text('./payment:bankName'), - :bic => bank.text('./payment:bic'), - :country_code => bank.text('./payment:countryCode'), - :iban => bank.text('./payment:iban'), - :owner_name => bank.text('./payment:ownerName') - } - end - end - - class XMLQuerier - NS = { - 'payment' => 'http://payment.services.adyen.com', - 'recurring' => 'http://recurring.services.adyen.com', - 'common' => 'http://common.services.adyen.com' - } - - class << self - attr_accessor :backend - - def backend=(backend) - @backend = backend - class_eval do - private - if backend == :nokogiri - def document_for_xml(xml) - Nokogiri::XML::Document.parse(xml) - end - def perform_xpath(query) - @node.xpath(query, NS) - end - else - def document_for_xml(xml) - REXML::Document.new(xml) - end - def perform_xpath(query) - REXML::XPath.match(@node, query, NS) - end - end + def params + @params ||= xml_querier.xpath('//recurring:listRecurringDetailsResponse/recurring:result') do |result| + { + :creation_date => DateTime.parse(result.text('./recurring:creationDate')), + :details => result.xpath('.//recurring:RecurringDetail').map { |node| parse_recurring_detail(node) }, + :last_known_shopper_email => result.text('./recurring:lastKnownShopperEmail'), + :shopper_reference => result.text('./recurring:shopperReference') + } end end - end - begin - require 'nokogiri' - self.backend = :nokogiri - rescue LoadError - require 'rexml/document' - self.backend = :rexml - end + private - def initialize(data) - @node = data.is_a?(String) ? document_for_xml(data) : data - end + # @todo add support for elv + def parse_recurring_detail(node) + result = { + :recurring_detail_reference => node.text('./recurring:recurringDetailReference'), + :variant => node.text('./recurring:variant'), + :creation_date => DateTime.parse(node.text('./recurring:creationDate')) + } - def xpath(query) - result = self.class.new(perform_xpath(query)) - block_given? ? yield(result) : result - end + card = node.xpath('./recurring:card') + if card.children.empty? + result[:bank] = parse_bank_details(node.xpath('./recurring:bank')) + else + result[:card] = parse_card_details(card) + end - def text(query) - xpath("#{query}/text()").to_s.strip - end + result + end - def children - @node.first.children - end + def parse_card_details(card) + { + :expiry_date => Date.new(card.text('./payment:expiryYear').to_i, card.text('./payment:expiryMonth').to_i, -1), + :holder_name => card.text('./payment:holderName'), + :number => card.text('./payment:number') + } + end - def empty? - @node.empty? + def parse_bank_details(bank) + { + :bank_account_number => bank.text('./payment:bankAccountNumber'), + :bank_location_id => bank.text('./payment:bankLocationId'), + :bank_name => bank.text('./payment:bankName'), + :bic => bank.text('./payment:bic'), + :country_code => bank.text('./payment:countryCode'), + :iban => bank.text('./payment:iban'), + :owner_name => bank.text('./payment:ownerName') + } + end end - - def to_s - @node.to_s - end - - def map(&block) - @node.map { |n| self.class.new(n) }.map(&block) - end end end end ######################## @@ -289,90 +380,93 @@ class PaymentService LAYOUT = <<EOS <?xml version="1.0"?> <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <soap:Body> - <ns1:authorise xmlns:ns1="http://payment.services.adyen.com"> - <ns1:paymentRequest> - <merchantAccount xmlns="http://payment.services.adyen.com">%s</merchantAccount> - <reference xmlns="http://payment.services.adyen.com">%s</reference> + <payment:authorise xmlns:payment="http://payment.services.adyen.com" xmlns:recurring="http://recurring.services.adyen.com" xmlns:common="http://common.services.adyen.com"> + <payment:paymentRequest> + <payment:merchantAccount>%s</payment:merchantAccount> + <payment:reference>%s</payment:reference> %s - </ns1:paymentRequest> - </ns1:authorise> + </payment:paymentRequest> + </payment:authorise> </soap:Body> </soap:Envelope> EOS AMOUNT_PARTIAL = <<EOS - <amount xmlns="http://payment.services.adyen.com"> - <currency xmlns="http://common.services.adyen.com">%s</currency> - <value xmlns="http://common.services.adyen.com">%s</value> - </amount> + <payment:amount> + <common:currency>%s</common:currency> + <common:value>%s</common:value> + </payment:amount> EOS CARD_PARTIAL = <<EOS - <card xmlns="http://payment.services.adyen.com"> - <holderName>%s</holderName> - <number>%s</number> - <cvc>%s</cvc> - <expiryYear>%s</expiryYear> - <expiryMonth>%02d</expiryMonth> - </card> + <payment:card> + <payment:holderName>%s</payment:holderName> + <payment:number>%s</payment:number> + <payment:cvc>%s</payment:cvc> + <payment:expiryYear>%s</payment:expiryYear> + <payment:expiryMonth>%02d</payment:expiryMonth> + </payment:card> EOS RECURRING_PARTIAL = <<EOS - <recurring xmlns="http://recurring.services.adyen.com"> - <contract xmlns="http://payment.services.adyen.com">RECURRING</contract> - </recurring> + <payment:recurring> + <payment:contract>RECURRING</payment:contract> + </payment:recurring> EOS - RECURRING_PAYMENT_BODY_PARTIAL = RECURRING_PARTIAL + <<EOS - <ns1:selectedRecurringDetailReference>%s</ns1:selectedRecurringDetailReference> - <ns1:shopperInteraction>ContAuth</ns1:shopperInteraction> + RECURRING_PAYMENT_BODY_PARTIAL = <<EOS + <payment:recurring> + <payment:contract>RECURRING</payment:contract> + </payment:recurring> + <payment:selectedRecurringDetailReference>%s</payment:selectedRecurringDetailReference> + <payment:shopperInteraction>ContAuth</payment:shopperInteraction> EOS SHOPPER_PARTIALS = { - :reference => ' <shopperReference xmlns="http://payment.services.adyen.com">%s</shopperReference>', - :email => ' <shopperEmail xmlns="http://payment.services.adyen.com">%s</shopperEmail>', - :ip => ' <shopperIP xmlns="http://payment.services.adyen.com">%s</shopperIP>', + :reference => ' <payment:shopperReference>%s</payment:shopperReference>', + :email => ' <payment:shopperEmail>%s</payment:shopperEmail>', + :ip => ' <payment:shopperIP>%s</payment:shopperIP>', } end class RecurringService LIST_LAYOUT = <<EOS <?xml version="1.0"?> <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <soap:Body> - <ns1:listRecurringDetails xmlns:ns1="http://recurring.services.adyen.com"> - <ns1:request> - <ns1:recurring> - <ns1:contract>RECURRING</ns1:contract> - </ns1:recurring> - <ns1:merchantAccount>%s</ns1:merchantAccount> - <ns1:shopperReference>%s</ns1:shopperReference> - </ns1:request> - </ns1:listRecurringDetails> + <recurring:listRecurringDetails xmlns:recurring="http://recurring.services.adyen.com"> + <recurring:request> + <recurring:recurring> + <recurring:contract>RECURRING</recurring:contract> + </recurring:recurring> + <recurring:merchantAccount>%s</recurring:merchantAccount> + <recurring:shopperReference>%s</recurring:shopperReference> + </recurring:request> + </recurring:listRecurringDetails> </soap:Body> </soap:Envelope> EOS DISABLE_LAYOUT = <<EOS <?xml version="1.0"?> <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <soap:Body> - <ns1:disable xmlns:ns1="http://recurring.services.adyen.com"> - <ns1:request> - <ns1:merchantAccount>%s</ns1:merchantAccount> - <ns1:shopperReference>%s</ns1:shopperReference> + <recurring:disable xmlns:recurring="http://recurring.services.adyen.com"> + <recurring:request> + <recurring:merchantAccount>%s</recurring:merchantAccount> + <recurring:shopperReference>%s</recurring:shopperReference> %s - </ns1:request> - </ns1:disable> + </recurring:request> + </recurring:disable> </soap:Body> </soap:Envelope> EOS RECURRING_DETAIL_PARTIAL = <<EOS - <ns1:recurringDetailReference>%s</ns1:recurringDetailReference> + <recurring:recurringDetailReference>%s</recurring:recurringDetailReference> EOS end end end