adyen 1.2.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -8,3 +8,4 @@ adyen-*.gem
8
8
  .DS_Store
9
9
  t.rb
10
10
  spec/functional/initializer.rb
11
+ Gemfile.lock
@@ -0,0 +1,8 @@
1
+ rvm:
2
+ - 1.8.7
3
+ - 1.9.2
4
+ - ruby-head
5
+ - ree
6
+ - rbx
7
+ - rbx-2.0
8
+ - jruby
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source :rubygems
2
+ gemspec
@@ -2,6 +2,8 @@
2
2
 
3
3
  Package to simplify including the Adyen payments services into a Ruby on Rails application.
4
4
 
5
+ Visit the wiki for documentation: https://github.com/wvanbergen/adyen/wiki.
6
+
5
7
  Adyen integration relies on three modes of communication between Adyen, your server and
6
8
  your client/customer:
7
9
 
@@ -13,30 +15,9 @@ This library aims to ease the implementation of all these modes into your applic
13
15
  Moreover, it provides matchers, assertions and mocks to make it easier to implement an
14
16
  automated test suite to assert the integration is working correctly.
15
17
 
16
- == Installation
17
-
18
- <b>Bundler / Rails 3</b>: Add the following line to your <tt>Gemfile</tt>:
19
-
20
- gem 'adyen'
21
-
22
- <b>Rails 2.x</b>: Add the following line to your <tt>environment.rb</tt> and run <tt>rake gems:install</tt>
23
- to make the Adyen functionality available in your Rails project:
24
-
25
- config.gem 'adyen'
26
-
27
- The Adyen gem will happily use REXML for communication with Adyen’s SOAP API, however, if
28
- you have the Nokogiri gem installed and required the gem will use that for performance.
29
-
30
- == Generators
31
-
32
- The gem ships with a generator for Rails 3. To create an ActiveRecord migration, mode, and
33
- ActionController for the notifications send by Adyen, run the following:
34
-
35
- $ rails generate adyen:notification
36
-
37
18
  == Usage
38
19
 
39
- See the project wiki on http://wiki.github.com/wvanbergen/adyen to get started. Complete
20
+ See the project wiki on https://github.com/wvanbergen/adyen/wiki to get started. Complete
40
21
  RDoc documentation for the project can be found on http://rdoc.info/projects/wvanbergen/adyen.
41
22
 
42
23
  * For more information about Adyen, see http://www.adyen.com
@@ -49,3 +30,13 @@ This package is written by Michel Barbosa and Willem van Bergen for Floorplanner
49
30
  made public under the MIT license (see LICENSE). Its is currently maintained by Willem van
50
31
  Bergen, Stefan Borsje and Eloy Duran. We are not affiliated with Adyen B.V. The software
51
32
  comes without warranty of any kind, so use at your own risk.
33
+
34
+ Contributions are welcomed; this is very much a scratch your own itch project. Some notes:
35
+
36
+ * Fork the project, implement your stuff and issue a pull request. Topic branches not necessary.
37
+ * All functionality must include tests and preferably documentation.
38
+ * New SOAP API calls should include functional tests that actually test if the call is working.
39
+ Adyen has a nasty tendency to switch things up every now and then, so this is vital.
40
+
41
+ Please visit the changelog at https://github.com/wvanbergen/adyen/wiki/Changelog to see the
42
+ changes in the different releases.
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'adyen'
3
- s.version = "1.2.0"
4
- s.date = "2011-05-12"
3
+ s.version = "1.3.0"
4
+ s.date = "2011-10-10"
5
5
 
6
6
  s.summary = "Integrate Adyen payment services in your Ruby on Rails application."
7
7
  s.description = <<-EOS
@@ -16,15 +16,22 @@ Gem::Specification.new do |s|
16
16
  s.homepage = 'http://github.com/wvanbergen/adyen/wiki'
17
17
 
18
18
  s.add_development_dependency('rake')
19
- s.add_development_dependency('rspec', '~> 2.0')
20
- s.add_development_dependency('nokogiri')
19
+ s.add_development_dependency('rspec', '~> 2')
21
20
  s.add_development_dependency('rails', '>= 2.3')
22
21
 
22
+ if RUBY_PLATFORM == 'java'
23
+ s.add_development_dependency('nokogiri', '~> 1.4.6')
24
+ else
25
+ s.add_development_dependency('nokogiri')
26
+ end
27
+
28
+ s.add_runtime_dependency('jruby-openssl') if RUBY_PLATFORM == 'java'
29
+
23
30
  s.requirements << 'Having Nokogiri installed will speed up XML handling when using the SOAP API.'
24
31
 
25
32
  s.rdoc_options << '--title' << s.name << '--main' << 'README.rdoc' << '--line-numbers' << '--inline-source'
26
33
  s.extra_rdoc_files = ['README.rdoc']
27
34
 
28
- s.files = %w(.gitignore .kick LICENSE README.rdoc Rakefile TODO adyen.gemspec lib/adyen.rb lib/adyen/api.rb lib/adyen/api/cacert.pem lib/adyen/api/payment_service.rb lib/adyen/api/recurring_service.rb lib/adyen/api/response.rb lib/adyen/api/simple_soap_client.rb lib/adyen/api/templates/payment_service.rb lib/adyen/api/templates/recurring_service.rb lib/adyen/api/test_helpers.rb lib/adyen/api/xml_querier.rb lib/adyen/configuration.rb lib/adyen/encoding.rb lib/adyen/form.rb lib/adyen/formatter.rb lib/adyen/matchers.rb lib/adyen/notification_generator.rb lib/adyen/railtie.rb lib/adyen/templates/notification_migration.rb lib/adyen/templates/notification_model.rb spec/adyen_spec.rb spec/api/api_spec.rb spec/api/payment_service_spec.rb spec/api/recurring_service_spec.rb spec/api/response_spec.rb spec/api/simple_soap_client_spec.rb spec/api/spec_helper.rb spec/api/test_helpers_spec.rb spec/form_spec.rb spec/functional/api_spec.rb spec/functional/initializer.rb.sample spec/spec_helper.rb tasks/github-gem.rake yard_extensions.rb)
35
+ s.files = %w(.gitignore .kick .travis.yml Gemfile LICENSE README.rdoc Rakefile TODO adyen.gemspec lib/adyen.rb lib/adyen/api.rb lib/adyen/api/cacert.pem lib/adyen/api/payment_service.rb lib/adyen/api/recurring_service.rb lib/adyen/api/response.rb lib/adyen/api/simple_soap_client.rb lib/adyen/api/templates/payment_service.rb lib/adyen/api/templates/recurring_service.rb lib/adyen/api/test_helpers.rb lib/adyen/api/xml_querier.rb lib/adyen/configuration.rb lib/adyen/encoding.rb lib/adyen/form.rb lib/adyen/formatter.rb lib/adyen/matchers.rb lib/adyen/notification_generator.rb lib/adyen/railtie.rb lib/adyen/templates/notification_migration.rb lib/adyen/templates/notification_model.rb spec/adyen_spec.rb spec/api/api_spec.rb spec/api/payment_service_spec.rb spec/api/recurring_service_spec.rb spec/api/response_spec.rb spec/api/simple_soap_client_spec.rb spec/api/spec_helper.rb spec/api/test_helpers_spec.rb spec/form_spec.rb spec/functional/api_spec.rb spec/functional/initializer.rb.sample spec/spec_helper.rb tasks/github-gem.rake yard_extensions.rb)
29
36
  s.test_files = %w(spec/adyen_spec.rb spec/api/api_spec.rb spec/api/payment_service_spec.rb spec/api/recurring_service_spec.rb spec/api/response_spec.rb spec/api/simple_soap_client_spec.rb spec/api/test_helpers_spec.rb spec/form_spec.rb spec/functional/api_spec.rb)
30
37
  end
@@ -12,7 +12,7 @@ module Adyen
12
12
  # Version constant for the Adyen plugin.
13
13
  # DO NOT CHANGE THIS VALUE BY HAND. It will be updated automatically by
14
14
  # the gem:release rake task.
15
- VERSION = "1.2.0"
15
+ VERSION = "1.3.0"
16
16
 
17
17
  # @return [Configuration] The configuration singleton.
18
18
  def self.configuration
@@ -24,5 +24,6 @@ require 'adyen/configuration'
24
24
  require 'adyen/encoding'
25
25
  require 'adyen/formatter'
26
26
  require 'adyen/form'
27
+ require 'adyen/api'
27
28
 
28
29
  require 'adyen/railtie' if defined?(::Rails) && ::Rails::VERSION::MAJOR >= 3
@@ -57,14 +57,15 @@ module Adyen
57
57
  # invoice.id,
58
58
  # { :currency => 'EUR', :value => invoice.amount },
59
59
  # { :reference => user.id, :email => user.email, :ip => '8.8.8.8' },
60
- # { :holder_name => "Simon Hopper", :number => '4444333322221111', :cvc => '737', :expiry_month => 12, :expiry_year => 2012 }
60
+ # { :holder_name => "Simon Hopper", :number => '4444333322221111', :cvc => '737',
61
+ # :expiry_month => 12, :expiry_year => 2012 }
61
62
  # )
62
63
  # response.authorised? # => true
63
64
  #
64
65
  # @param [Numeric,String] reference Your reference (ID) for this payment.
65
66
  # @param [Hash] amount A hash describing the money to charge.
66
67
  # @param [Hash] shopper A hash describing the shopper.
67
- # @param [Hash] card A hash describing the creditcard details.
68
+ # @param [Hash] card A hash describing the credit card details.
68
69
  #
69
70
  # @option amount [String] :currency The ISO currency code (EUR, GBP, USD, etc).
70
71
  # @option amount [Integer] :value The value of the payment in discrete cents,
@@ -278,15 +279,25 @@ module Adyen
278
279
  }).disable
279
280
  end
280
281
 
281
- # Stores and tokenises the creditcard details so that recurring payments can be made in the
282
- # future.
282
+ # Stores and tokenises the payment details so that recurring payments can be made in the
283
+ # future. It can be either a credit card or ELV (Elektronisches Lastschriftverfahren).
283
284
  #
284
- # You do *not* have to include the card's CVC, because it won't be stored anyway.
285
+ # For instance, this is how you would store credit card details:
285
286
  #
286
287
  # # @example
287
288
  # response = Adyen::API.store_recurring_token(
288
289
  # { :reference => user.id, :email => user.email, :ip => '8.8.8.8' },
289
- # { :holder_name => "Simon Hopper", :number => '4444333322221111', :expiry_month => 12, :expiry_year => 2012 }
290
+ # { :holder_name => "Simon Hopper", :number => '4444333322221111',
291
+ # :expiry_month => 12, :expiry_year => 2012 }
292
+ # )
293
+ #
294
+ # Or use the following to store ELV details:
295
+ #
296
+ # # @example
297
+ # response = Adyen::API.store_recurring_token(
298
+ # { :reference => user.id, :email => user.email, :ip => '8.8.8.8' },
299
+ # { :bank_location => "Berlin", :bank_name => "TestBank", :bank_location_id => "12345678",
300
+ # :holder_name => "Simon Hopper", :number => "1234567890" }
290
301
  # )
291
302
  # response.stored? # => true
292
303
  #
@@ -299,24 +310,35 @@ module Adyen
299
310
  # )
300
311
  # authorize_response.authorised? # => true
301
312
  #
302
- # @param [Hash] shopper A hash describing the shopper.
303
- # @param [Hash] card A hash describing the creditcard details.
313
+ # @param [Hash] params A hash describing the credit card or
314
+ # ELV details.
304
315
  #
305
- # @option shopper [Numeric,String] :reference The shopper’s reference (ID).
306
- # @option shopper [String] :email The shopper’s email address.
307
- # @option shopper [String] :ip The shopper’s IP address.
316
+ # @option shopper [Numeric,String] :reference The shopper’s reference (ID).
317
+ # @option shopper [String] :email The shopper’s email address.
318
+ # @option shopper [String] :ip The shopper’s IP address.
308
319
  #
309
- # @option card [String] :holder_name The full name on the card.
310
- # @option card [String] :number The card number.
311
- # @option card [Numeric,String] :expiry_month The month in which the card expires.
312
- # @option card [Numeric,String] :expiry_year The year in which the card expires.
320
+ # @option params [String] :holder_name The full name on the card or of the
321
+ # account holder.
322
+ # @option params [String] :number The card or account number.
323
+ #
324
+ # ##### Credit card specific options:
325
+ #
326
+ # @option params [Numeric,String] :expiry_month The month in which the card expires.
327
+ # @option params [Numeric,String] :expiry_year The year in which the card expires.
328
+ #
329
+ # ##### ELV specific options:
330
+ #
331
+ # @option params [String] :bank_location The Bank Location.
332
+ # @option params [String] :bank_name The Bank Name.
333
+ # @option params [Numeric,String] :bank_location_id The Bank Location ID (Bankleitzahl).
313
334
  #
314
335
  # @return [RecurringService::StoreTokenResponse] The response object
315
- def store_recurring_token(shopper, card)
316
- RecurringService.new({
317
- :shopper => shopper,
318
- :card => card
319
- }).store_token
336
+ def store_recurring_token(shopper, params)
337
+ payment_method = params.include?(:bank_location_id) ? :elv : :card
338
+ RecurringService.new({
339
+ :shopper => shopper,
340
+ payment_method => params
341
+ }).store_token
320
342
  end
321
343
  end
322
344
  end
@@ -158,14 +158,7 @@ module Adyen
158
158
  }
159
159
 
160
160
  AUTHORISED = 'Authorised'
161
-
162
- def self.original_fault_message_for(attribute, message)
163
- if error = ERRORS.find { |_, (a, m)| a == attribute && m == message }
164
- error.first
165
- else
166
- message
167
- end
168
- end
161
+ REFUSED = 'Refused'
169
162
 
170
163
  response_attrs :result_code, :auth_code, :refusal_reason, :psp_reference
171
164
 
@@ -173,7 +166,12 @@ module Adyen
173
166
  super && params[:result_code] == AUTHORISED
174
167
  end
175
168
 
176
- alias authorized? success?
169
+ def refused?
170
+ params[:result_code] == REFUSED
171
+ end
172
+
173
+ alias_method :authorised?, :success?
174
+ alias_method :authorized?, :success?
177
175
 
178
176
  # @return [Boolean] Returns whether or not the request was valid.
179
177
  def invalid_request?
@@ -193,8 +191,12 @@ module Adyen
193
191
  def error(prefix = nil)
194
192
  if error = ERRORS[fault_message]
195
193
  prefix ? ["#{prefix}_#{error[0]}".to_sym, error[1]] : error
196
- else
194
+ elsif fault_message
197
195
  [:base, fault_message]
196
+ elsif refused?
197
+ [:base, 'Transaction was refused.']
198
+ else
199
+ [:base, 'Transaction failed for unkown reasons.']
198
200
  end
199
201
  end
200
202
 
@@ -46,6 +46,14 @@ module Adyen
46
46
  CARD_PARTIAL % card
47
47
  end
48
48
 
49
+ ELV_ATTRS = [:bank_location, :bank_name, :bank_location_id, :holder_name, :number]
50
+ # The ELV - (Elektronisches Lastschriftverfahren) does not require bank_location, so insert 'nil'.
51
+ def elv_partial
52
+ validate_parameters!(:elv => ELV_ATTRS)
53
+ elv = @params[:elv].values_at(*ELV_ATTRS)
54
+ ELV_PARTIAL % elv
55
+ end
56
+
49
57
  def list_request_body
50
58
  validate_parameters!(:merchant_account, :shopper => [:reference])
51
59
  LIST_LAYOUT % [@params[:merchant_account], @params[:shopper][:reference]]
@@ -61,7 +69,10 @@ module Adyen
61
69
 
62
70
  def store_token_request_body
63
71
  validate_parameters!(:merchant_account, :shopper => [:email, :reference])
64
- content = card_partial
72
+ content = []
73
+ content << card_partial unless @params[:card].nil?
74
+ content << elv_partial unless @params[:elv].nil?
75
+ raise ArgumentError, "The required parameter 'card' or 'elv' is missing." if content.empty?
65
76
  STORE_TOKEN_LAYOUT % [@params[:merchant_account], @params[:shopper][:reference], @params[:shopper][:email], content]
66
77
  end
67
78
 
@@ -74,7 +85,7 @@ module Adyen
74
85
  super && DISABLED_RESPONSES.include?(params[:response])
75
86
  end
76
87
 
77
- alias disabled? success?
88
+ alias_method :disabled?, :success?
78
89
 
79
90
  def params
80
91
  @params ||= { :response => xml_querier.text('//recurring:disableResponse/recurring:result/recurring:response') }
@@ -111,12 +122,17 @@ module Adyen
111
122
  }
112
123
 
113
124
  card = node.xpath('./recurring:card')
114
- if card.children.empty?
115
- result[:bank] = parse_bank_details(node.xpath('./recurring:bank'))
116
- else
125
+ elv = node.xpath('./recurring:elv')
126
+ bank = node.xpath('./recurring:bank')
127
+
128
+ if !card.children.empty?
117
129
  result[:card] = parse_card_details(card)
130
+ elsif !elv.children.empty?
131
+ result[:elv] = parse_elv_details(elv)
132
+ else
133
+ result[:bank] = parse_bank_details(bank)
118
134
  end
119
-
135
+
120
136
  result
121
137
  end
122
138
 
@@ -128,15 +144,25 @@ module Adyen
128
144
  }
129
145
  end
130
146
 
147
+ def parse_elv_details(elv)
148
+ {
149
+ :holder_name => bank.text('./payment:accountHolderName'),
150
+ :number => bank.text('./payment:bankAccountNumber'),
151
+ :bank_location => bank.text('./payment:bankLocation'),
152
+ :bank_location_id => bank.text('./payment:bankLocationId'),
153
+ :bank_name => bank.text('./payment:bankName')
154
+ }
155
+ end
156
+
131
157
  def parse_bank_details(bank)
132
158
  {
133
- :bank_account_number => bank.text('./payment:bankAccountNumber'),
134
- :bank_location_id => bank.text('./payment:bankLocationId'),
135
- :bank_name => bank.text('./payment:bankName'),
136
- :bic => bank.text('./payment:bic'),
137
- :country_code => bank.text('./payment:countryCode'),
138
- :iban => bank.text('./payment:iban'),
139
- :owner_name => bank.text('./payment:ownerName')
159
+ :number => bank.text('./payment:bankAccountNumber'),
160
+ :bank_location_id => bank.text('./payment:bankLocationId'),
161
+ :bank_name => bank.text('./payment:bankName'),
162
+ :bic => bank.text('./payment:bic'),
163
+ :country_code => bank.text('./payment:countryCode'),
164
+ :iban => bank.text('./payment:iban'),
165
+ :holder_name => bank.text('./payment:ownerName')
140
166
  }
141
167
  end
142
168
  end
@@ -152,12 +178,14 @@ module Adyen
152
178
  super && params[:response] == 'Success'
153
179
  end
154
180
 
155
- alias stored? success?
181
+ alias_method :stored?, :success?
156
182
 
157
183
  def params
158
- @params ||= { :response => xml_querier.text('//recurring:storeTokenResponse/recurring:result/recurring:result'),
184
+ @params ||= {
185
+ :response => xml_querier.text('//recurring:storeTokenResponse/recurring:result/recurring:result'),
159
186
  :reference => xml_querier.text('//recurring:storeTokenResponse/recurring:result/recurring:rechargeReference'),
160
- :recurring_detail_reference => xml_querier.text('//recurring:storeTokenResponse/recurring:result/recurring:recurringDetailReference')}
187
+ :recurring_detail_reference => xml_querier.text('//recurring:storeTokenResponse/recurring:result/recurring:recurringDetailReference')
188
+ }
161
189
  end
162
190
  end
163
191
  end
@@ -32,6 +32,13 @@ module Adyen
32
32
  !@http_response.is_a?(Net::HTTPSuccess)
33
33
  end
34
34
 
35
+ # @return [Boolean] Whether or not the SOAP request itself was a success.
36
+ # Adyen returns a 500 status code for e.g. failed CC validation and in this case, we don't
37
+ # want to throw a server error but rather treat it as something normal.
38
+ def server_error?
39
+ @http_response.is_a?(Net::HTTPServerError) && fault_message.nil?
40
+ end
41
+
35
42
  # @return [XMLQuerier] The response body wrapped in a XMLQuerier.
36
43
  def xml_querier
37
44
  @xml_querier ||= XMLQuerier.new(@http_response.body)
@@ -22,23 +22,27 @@ EOS
22
22
  # @see http://curl.haxx.se/ca/cacert.pem
23
23
  CACERT = File.expand_path('../cacert.pem', __FILE__)
24
24
 
25
- class ClientError < StandardError
25
+ class StandardError < ::StandardError
26
26
  def initialize(response, action, endpoint)
27
27
  @response, @action, @endpoint = response, action, endpoint
28
28
  end
29
29
 
30
- def message
31
- "[#{@response.code} #{@response.message}] A client error occurred while calling SOAP action `#{@action}' on endpoint `#{@endpoint}'."
30
+ private
31
+
32
+ def message_prefix
33
+ %{[#{@response.http_response.code} #{@response.http_response.message}] A %s error occurred while calling SOAP action `#{@action}' on endpoint `#{@endpoint}'.}
32
34
  end
33
35
  end
34
36
 
35
- class ServerError < StandardError
36
- def initialize(response, action, endpoint)
37
- @response, @action, @endpoint = response, action, endpoint
37
+ class ClientError < StandardError
38
+ def message
39
+ "#{message_prefix % "client"} Fault message: #{@response.fault_message}."
38
40
  end
41
+ end
39
42
 
43
+ class ServerError < StandardError
40
44
  def message
41
- "[#{@response.code} #{@response.message}] A server error occurred while calling SOAP action `#{@action}' on endpoint `#{@endpoint}'."
45
+ message_prefix % 'server'
42
46
  end
43
47
  end
44
48
 
@@ -118,9 +122,10 @@ EOS
118
122
 
119
123
  request.start do |http|
120
124
  http_response = http.request(post)
121
- raise ClientError.new(http_response, action, endpoint) if http_response.is_a?(Net::HTTPClientError)
122
- raise ServerError.new(http_response, action, endpoint) if http_response.is_a?(Net::HTTPServerError)
123
- response_class.new(http_response)
125
+ response = response_class.new(http_response)
126
+ raise ClientError.new(response, action, endpoint) if http_response.is_a?(Net::HTTPClientError)
127
+ raise ServerError.new(response, action, endpoint) if response.server_error?
128
+ response
124
129
  end
125
130
  end
126
131
  end
@@ -54,6 +54,18 @@ EOS
54
54
  <payment:expiryMonth>%02d</payment:expiryMonth>
55
55
  </recurring:card>
56
56
  EOS
57
+ # Electronic bank debit in Germany. Semi real-time payment method.
58
+ # @private
59
+ ELV_PARTIAL = <<EOS
60
+ <recurring:elv>
61
+ <payment:bankLocation>%s</payment:bankLocation>
62
+ <payment:bankName>%s</payment:bankName>
63
+ <payment:bankLocationId>%s</payment:bankLocationId>
64
+ <payment:accountHolderName>%s</payment:accountHolderName>
65
+ <payment:bankAccountNumber>%02d</payment:bankAccountNumber>
66
+ </recurring:elv>
67
+ EOS
68
+
57
69
  end
58
70
  end
59
71
  end
@@ -56,7 +56,7 @@ class AdyenNotification < ActiveRecord::Base
56
56
  event_code == 'AUTHORISATION'
57
57
  end
58
58
 
59
- alias :authorization? :authorisation?
59
+ alias_method :authorization?, :authorisation?
60
60
 
61
61
  # Returns true if this notification is an AUTHORISATION notification and
62
62
  # the success status indicates that the authorization was successfull.
@@ -66,5 +66,5 @@ class AdyenNotification < ActiveRecord::Base
66
66
  event_code == 'AUTHORISATION' && success?
67
67
  end
68
68
 
69
- alias :successful_authorization? :successful_authorisation?
69
+ alias_method :successful_authorization?, :successful_authorisation?
70
70
  end
@@ -131,6 +131,17 @@ describe Adyen::API do
131
131
  )
132
132
  end
133
133
 
134
+ it "performs a `tokenize ELV details' request" do
135
+ should_map_shortcut_to(:store_token,
136
+ :shopper => { :reference => 'user-id', :email => 's.hopper@example.com' },
137
+ :elv => { :bank_location => "Berlin", :bank_name => "TestBank", :bank_location_id => "12345678", :holder_name => "Simon Hopper", :number => "1234567890" }
138
+ )
139
+ Adyen::API.store_recurring_token(
140
+ { :reference => 'user-id', :email => 's.hopper@example.com' },
141
+ { :bank_location => "Berlin", :bank_name => "TestBank", :bank_location_id => "12345678", :holder_name => "Simon Hopper", :number => "1234567890" }
142
+ )
143
+ end
144
+
134
145
  it "preforms a `list recurring details' request" do
135
146
  should_map_shortcut_to(:list, :shopper => { :reference => 'user-id' })
136
147
  Adyen::API.list_recurring_details('user-id')
@@ -156,6 +156,18 @@ describe Adyen::API::PaymentService do
156
156
  end
157
157
  end
158
158
 
159
+ describe "with a `refused' response" do
160
+ before do
161
+ stub_net_http(AUTHORISE_REQUEST_REFUSED_RESPONSE)
162
+ @response = @payment.authorise_payment
163
+ end
164
+
165
+ it "returns that the payment was refused" do
166
+ @response.should be_refused
167
+ @response.error.should == [:base, 'Transaction was refused.']
168
+ end
169
+ end
170
+
159
171
  describe "with a `invalid' response" do
160
172
  before do
161
173
  stub_net_http(AUTHORISE_REQUEST_INVALID_RESPONSE % 'validation 101 Invalid card number')
@@ -202,19 +214,6 @@ describe Adyen::API::PaymentService do
202
214
  end
203
215
  end
204
216
 
205
- it "returns the original message corresponding to the given attribute and message" do
206
- [
207
- ["validation 101 Invalid card number", [:number, 'is not a valid creditcard number']],
208
- ["validation 103 CVC is not the right length", [:cvc, 'is not the right length']],
209
- ["validation 128 Card Holder Missing", [:holder_name, 'can\'t be blank']],
210
- ["validation Couldn't parse expiry year", [:expiry_year, 'could not be recognized']],
211
- ["validation Expiry month should be between 1 and 12 inclusive", [:expiry_month, 'could not be recognized']],
212
- ["validation 130 Reference Missing", [:base, 'validation 130 Reference Missing']],
213
- ].each do |expected, attr_and_message|
214
- Adyen::API::PaymentService::AuthorisationResponse.original_fault_message_for(*attr_and_message).should == expected
215
- end
216
- end
217
-
218
217
  private
219
218
 
220
219
  def response_with_fault_message(message)
@@ -27,6 +27,14 @@ describe Adyen::API::RecurringService do
27
27
  #:issue_number => ,
28
28
  #:start_month => ,
29
29
  #:start_year => ,
30
+ },
31
+ # German's Direct Debit (Elektronisches Lastschriftverfahren)
32
+ :elv => {
33
+ :holder_name => 'Simon わくわく Hopper',
34
+ :number => '1234567890',
35
+ :bank_location => 'Berlin',
36
+ :bank_location_id => '12345678',
37
+ :bank_name => 'TestBank',
30
38
  }
31
39
  }
32
40
  @recurring = @object = Adyen::API::RecurringService.new(@params)
@@ -67,13 +75,13 @@ describe Adyen::API::RecurringService do
67
75
  },
68
76
  {
69
77
  :bank => {
70
- :bank_account_number => '123456789',
78
+ :number => '123456789',
71
79
  :bank_location_id => 'bank-location-id',
72
80
  :bank_name => 'AnyBank',
73
81
  :bic => 'BBBBCCLLbbb',
74
82
  :country_code => 'NL',
75
83
  :iban => 'NL69PSTB0001234567',
76
- :owner_name => 'S. Hopper'
84
+ :holder_name => 'S. Hopper'
77
85
  },
78
86
  :recurring_detail_reference => 'RecurringDetailReference2',
79
87
  :variant => 'IDEAL',
@@ -161,6 +169,38 @@ describe Adyen::API::RecurringService do
161
169
  text('./recurring:recurring/payment:contract').should == 'RECURRING'
162
170
  end
163
171
  end
172
+
173
+ describe_request_body_of :store_token, '//recurring:storeToken/recurring:request' do
174
+ it_should_validate_request_parameters :merchant_account,
175
+ :shopper => [:email, :reference]
176
+
177
+ it "includes the merchant account handle" do
178
+ text('./recurring:merchantAccount').should == 'SuperShopper'
179
+ end
180
+
181
+ it "includes the shopper’s reference" do
182
+ text('./recurring:shopperReference').should == 'user-id'
183
+ end
184
+
185
+ it "includes the shopper’s email" do
186
+ text('./recurring:shopperEmail').should == 's.hopper@example.com'
187
+ end
188
+
189
+ it "includes the ELV details" do
190
+ xpath('./recurring:elv') do |elv|
191
+ # there's no reason why Nokogiri should escape these characters, but as long as they're correct
192
+ elv.text('./payment:accountHolderName').should == 'Simon &#x308F;&#x304F;&#x308F;&#x304F; Hopper'
193
+ elv.text('./payment:bankAccountNumber').should == '1234567890'
194
+ elv.text('./payment:bankLocation').should == 'Berlin'
195
+ elv.text('./payment:bankLocationId').should == '12345678'
196
+ elv.text('./payment:bankName').should == 'TestBank'
197
+ end
198
+ end
199
+
200
+ it "includes the necessary recurring and one-click contract info if the `:recurring' param is truthful" do
201
+ text('./recurring:recurring/payment:contract').should == 'RECURRING'
202
+ end
203
+ end
164
204
 
165
205
  describe_response_from :disable, (DISABLE_RESPONSE % '[detail-successfully-disabled]'), 'disable' do
166
206
  it "returns whether or not it was disabled" do
@@ -32,4 +32,28 @@ describe Adyen::API::Response do
32
32
  @response.should_not be_a_success
33
33
  end
34
34
  end
35
+
36
+ describe "with a server error HTTP response and _no_ SOAP fault message" do
37
+ before do
38
+ http_response = Net::HTTPServerError.new('1.1', '500', 'Internal Server Error')
39
+ http_response.stub!(:body).and_return(%{<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></soap:Body></soap:Envelope>})
40
+ @response = Adyen::API::Response.new(http_response)
41
+ end
42
+
43
+ it "`server_error?` returns that the (HTTP) request did cause a server error" do
44
+ @response.server_error?.should be_true
45
+ end
46
+ end
47
+
48
+ describe "with a server error HTTP response _and_ SOAP fault message" do
49
+ before do
50
+ http_response = Net::HTTPServerError.new('1.1', '500', 'Internal Server Error')
51
+ http_response.stub!(:body).and_return(%{<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><soap:Fault><faultcode>soap:Server</faultcode><faultstring>Illegal argument. For input string: "100.0"</faultstring></soap:Fault></soap:Body></soap:Envelope>})
52
+ @response = Adyen::API::Response.new(http_response)
53
+ end
54
+
55
+ it "`server_error?` returns that the (HTTP) request did not cause a server error" do
56
+ @response.server_error?.should be_false
57
+ end
58
+ end
35
59
  end
@@ -76,16 +76,58 @@ describe Adyen::API::SimpleSOAPClient do
76
76
  @response.xml_querier.to_s.should == AUTHORISE_RESPONSE
77
77
  end
78
78
 
79
- it "raises when the HTTP response is a subclass of Net::HTTPClientError" do
80
- Net::HTTP.stubbed_response = Net::HTTPBadRequest.new('1.1', '401', 'Bad request')
81
- exception = nil
82
- begin
83
- @client.call_webservice_action('Action', '<bananas>Yes, please</bananas>', Adyen::API::Response)
84
- rescue Adyen::API::SimpleSOAPClient::ClientError => e
85
- exception = e
79
+ [
80
+ [
81
+ "[401 Bad request] A client",
82
+ Net::HTTPBadRequest.new('1.1', '401', 'Bad request'),
83
+ Adyen::API::SimpleSOAPClient::ClientError
84
+ ]
85
+ ].each do |label, response, expected_exception|
86
+ it "raises when the HTTP response is a subclass of #{response.class.name}" do
87
+ response.stub!(:body).and_return(%{<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><soap:Fault><faultcode>soap:Server</faultcode><faultstring>Illegal argument. For input string: "100.0"</faultstring></soap:Fault></soap:Body></soap:Envelope>})
88
+ Net::HTTP.stubbed_response = response
89
+
90
+ exception = nil
91
+ begin
92
+ @client.call_webservice_action('Action', '<bananas>Yes, please</bananas>', Adyen::API::Response)
93
+ rescue expected_exception => e
94
+ exception = e
95
+ end
96
+ exception.message.should == %{#{label} error occurred while calling SOAP action `Action' on endpoint `https://test.example.com/soap/Action'. Fault message: Illegal argument. For input string: "100.0".}
97
+ end
98
+ end
99
+
100
+ describe 'server error' do
101
+ [
102
+ ["[500 Internal Server Error] A server", Net::HTTPBadGateway.new('1.1', '500', 'Internal Server Error')],
103
+ ["[501 Not Implemented] A server", Net::HTTPBadGateway.new('1.1', '501', 'Not Implemented')],
104
+ ["[502 Bad Gateway] A server", Net::HTTPBadGateway.new('1.1', '502', 'Bad Gateway')],
105
+ ["[503 Service Unavailable] A server", Net::HTTPBadGateway.new('1.1', '503', 'Service Unavailable')],
106
+ ["[504 Gateway Timeout] A server", Net::HTTPBadGateway.new('1.1', '504', 'Gateway Timeout')],
107
+ ["[505 HTTP Version Not Supported] A server", Net::HTTPBadGateway.new('1.1', '505', 'HTTP Version Not Supported')],
108
+ ].each do |label, response|
109
+ it "is raised when the HTTP response is a `real` server error by status code" do
110
+ response.stub!(:body).and_return(%{<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></soap:Body></soap:Envelope>})
111
+ Net::HTTP.stubbed_response = response
112
+
113
+ exception = nil
114
+ begin
115
+ @client.call_webservice_action('Action', '<bananas>Yes, please</bananas>', Adyen::API::Response)
116
+ rescue Adyen::API::SimpleSOAPClient::ServerError => e
117
+ exception = e
118
+ end
119
+ exception.message.should == %{#{label} error occurred while calling SOAP action `Action' on endpoint `https://test.example.com/soap/Action'.}
120
+ end
121
+ end
122
+
123
+ it "is not raised when the HTTP response has a 500 status code with a fault message" do
124
+ response = Net::HTTPServerError.new('1.1', '500', 'Internal Server Error')
125
+ response.stub!(:body).and_return(%{<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><soap:Fault><faultcode>soap:Server</faultcode><faultstring>Illegal argument. For input string: "100.0"</faultstring></soap:Fault></soap:Body></soap:Envelope>})
126
+
127
+ lambda do
128
+ @client.call_webservice_action('Action', '<bananas>Yes, please</bananas>', Adyen::API::Response)
129
+ end.should_not raise_error Adyen::API::SimpleSOAPClient::ServerError
86
130
  end
87
- msg = "[401 Bad request] A client error occurred while calling SOAP action `Action' on endpoint `https://test.example.com/soap/Action'."
88
- exception.message.should == msg
89
131
  end
90
132
  end
91
133
  end
@@ -274,6 +274,21 @@ AUTHORISE_REQUEST_INVALID_RESPONSE = <<EOS
274
274
  </soap:Envelope>
275
275
  EOS
276
276
 
277
+ AUTHORISE_REQUEST_REFUSED_RESPONSE = <<EOS
278
+ <?xml version="1.0" encoding="UTF-8"?>
279
+ <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">
280
+ <soap:Body>
281
+ <ns1:authoriseResponse xmlns:ns1="http://payment.services.adyen.com">
282
+ <ns1:paymentResult>
283
+ <refusalReason xmlns="http://payment.services.adyen.com">You need to actually own money.</refusalReason>
284
+ <resultCode xmlns="http://payment.services.adyen.com">Refused</resultCode>
285
+ </ns1:paymentResult>
286
+ </ns1:authoriseResponse>
287
+ </soap:Body>
288
+ </soap:Envelope>
289
+ EOS
290
+
291
+
277
292
  LIST_RESPONSE = <<EOS
278
293
  <?xml version="1.0"?>
279
294
  <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">
@@ -57,14 +57,23 @@ if File.exist?(API_SPEC_INITIALIZER)
57
57
  response.psp_reference.should_not be_empty
58
58
  end
59
59
 
60
- it "stores the provided creditcard details" do
61
- response = Adyen::API.store_recurring_token(
62
- { :email => "#{@user_id}@example.com", :reference => @user_id },
63
- { :expiry_month => 12, :expiry_year => 2012, :holder_name => "Simon #{@user_id} Hopper", :number => '4111111111111111' }
64
- )
65
- response.should be_stored
66
- response.recurring_detail_reference.should_not be_empty
67
- end
60
+ # TODO disabled for now: https://github.com/wvanbergen/adyen/issues/29
61
+ #it "stores the provided ELV account details" do
62
+ #response = Adyen::API.store_recurring_token(
63
+ #{ :email => "#{@user_id}@example.com", :reference => @user_id },
64
+ #{ :bank_location => "Berlin", :bank_name => "TestBank", :bank_location_id => "12345678", :holder_name => "Simon #{@user_id} Hopper", :number => "1234567890" }
65
+ #)
66
+ #response.should be_stored
67
+ #response.recurring_detail_reference.should_not be_empty
68
+ #end
69
+ #it "stores the provided creditcard details" do
70
+ #response = Adyen::API.store_recurring_token(
71
+ #{ :email => "#{@user_id}@example.com", :reference => @user_id },
72
+ #{ :expiry_month => 12, :expiry_year => 2012, :holder_name => "Simon #{@user_id} Hopper", :number => '4111111111111111' }
73
+ #)
74
+ #response.should be_stored
75
+ #response.recurring_detail_reference.should_not be_empty
76
+ #end
68
77
 
69
78
  it "captures a payment" do
70
79
  response = Adyen::API.capture_payment(@payment_response.psp_reference, { :currency => 'EUR', :value => '1234' })
@@ -1,10 +1,8 @@
1
1
  # encoding: UTF-8
2
2
 
3
3
  require 'rubygems'
4
- require 'rspec'
5
-
6
- $:.unshift File.expand_path('../../lib', __FILE__)
7
4
 
5
+ require 'rspec'
8
6
  require 'adyen'
9
7
  require 'adyen/matchers'
10
8
 
@@ -13,8 +13,8 @@ module GithubGem
13
13
 
14
14
  # Detects the main include file of this project using heuristics
15
15
  def self.detect_main_include
16
- if detect_gemspec_file =~ /^(\.*)\.gemspec$/ && File.exist?("lib/#{$1}.rb")
17
- "lib/#{$1}.rb"
16
+ if File.exist?(File.expand_path("../lib/#{File.basename(detect_gemspec_file, '.gemspec').gsub(/-/, '/')}.rb", detect_gemspec_file))
17
+ "lib/#{File.basename(detect_gemspec_file, '.gemspec').gsub(/-/, '/')}.rb"
18
18
  elsif FileList['lib/*.rb'].length == 1
19
19
  FileList['lib/*.rb'].first
20
20
  else
@@ -24,6 +24,8 @@ module GithubGem
24
24
 
25
25
  class RakeTasks
26
26
 
27
+ include Rake::DSL if Rake.const_defined?('DSL')
28
+
27
29
  attr_reader :gemspec, :modified_files
28
30
  attr_accessor :gemspec_file, :task_namespace, :main_include, :root_dir, :spec_pattern, :test_pattern, :remote, :remote_branch, :local_branch
29
31
 
@@ -331,7 +333,7 @@ module GithubGem
331
333
 
332
334
  # Reload the gemspec so the changes are incorporated
333
335
  load_gemspec!
334
-
336
+
335
337
  # Also mark the Gemfile.lock file as changed because of the new version.
336
338
  modified_files << 'Gemfile.lock' if File.exist?(File.join(root_dir, 'Gemfile.lock'))
337
339
  end
@@ -341,8 +343,8 @@ module GithubGem
341
343
  def update_tasks_task
342
344
  require 'net/https'
343
345
  require 'uri'
344
-
345
- uri = URI.parse('https://github.com/wvanbergen/github-gem/raw/master/tasks/github-gem.rake')
346
+
347
+ uri = URI.parse('https://raw.github.com/wvanbergen/github-gem/master/tasks/github-gem.rake')
346
348
  http = Net::HTTP.new(uri.host, uri.port)
347
349
  http.use_ssl = true
348
350
  http.verify_mode = OpenSSL::SSL::VERIFY_NONE
metadata CHANGED
@@ -1,12 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: adyen
3
3
  version: !ruby/object:Gem::Version
4
- prerelease: false
4
+ hash: 27
5
+ prerelease:
5
6
  segments:
6
7
  - 1
7
- - 2
8
+ - 3
8
9
  - 0
9
- version: 1.2.0
10
+ version: 1.3.0
10
11
  platform: ruby
11
12
  authors:
12
13
  - Willem van Bergen
@@ -17,8 +18,7 @@ autorequire:
17
18
  bindir: bin
18
19
  cert_chain: []
19
20
 
20
- date: 2011-05-12 00:00:00 -04:00
21
- default_executable:
21
+ date: 2011-10-10 00:00:00 Z
22
22
  dependencies:
23
23
  - !ruby/object:Gem::Dependency
24
24
  name: rake
@@ -28,6 +28,7 @@ dependencies:
28
28
  requirements:
29
29
  - - ">="
30
30
  - !ruby/object:Gem::Version
31
+ hash: 3
31
32
  segments:
32
33
  - 0
33
34
  version: "0"
@@ -41,37 +42,39 @@ dependencies:
41
42
  requirements:
42
43
  - - ~>
43
44
  - !ruby/object:Gem::Version
45
+ hash: 7
44
46
  segments:
45
47
  - 2
46
- - 0
47
- version: "2.0"
48
+ version: "2"
48
49
  type: :development
49
50
  version_requirements: *id002
50
51
  - !ruby/object:Gem::Dependency
51
- name: nokogiri
52
+ name: rails
52
53
  prerelease: false
53
54
  requirement: &id003 !ruby/object:Gem::Requirement
54
55
  none: false
55
56
  requirements:
56
57
  - - ">="
57
58
  - !ruby/object:Gem::Version
59
+ hash: 5
58
60
  segments:
59
- - 0
60
- version: "0"
61
+ - 2
62
+ - 3
63
+ version: "2.3"
61
64
  type: :development
62
65
  version_requirements: *id003
63
66
  - !ruby/object:Gem::Dependency
64
- name: rails
67
+ name: nokogiri
65
68
  prerelease: false
66
69
  requirement: &id004 !ruby/object:Gem::Requirement
67
70
  none: false
68
71
  requirements:
69
72
  - - ">="
70
73
  - !ruby/object:Gem::Version
74
+ hash: 3
71
75
  segments:
72
- - 2
73
- - 3
74
- version: "2.3"
76
+ - 0
77
+ version: "0"
75
78
  type: :development
76
79
  version_requirements: *id004
77
80
  description: " Package to simplify including the Adyen payments services into a Ruby on Rails application.\n The package provides functionality to create payment forms, handling and storing notifications \n sent by Adyen and consuming the SOAP services provided by Adyen. Moreover, it contains helper\n methods, mocks and matchers to simpify writing tests/specsfor your code.\n"
@@ -89,6 +92,8 @@ extra_rdoc_files:
89
92
  files:
90
93
  - .gitignore
91
94
  - .kick
95
+ - .travis.yml
96
+ - Gemfile
92
97
  - LICENSE
93
98
  - README.rdoc
94
99
  - Rakefile
@@ -128,7 +133,6 @@ files:
128
133
  - spec/spec_helper.rb
129
134
  - tasks/github-gem.rake
130
135
  - yard_extensions.rb
131
- has_rdoc: true
132
136
  homepage: http://github.com/wvanbergen/adyen/wiki
133
137
  licenses: []
134
138
 
@@ -147,6 +151,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
147
151
  requirements:
148
152
  - - ">="
149
153
  - !ruby/object:Gem::Version
154
+ hash: 3
150
155
  segments:
151
156
  - 0
152
157
  version: "0"
@@ -155,13 +160,14 @@ required_rubygems_version: !ruby/object:Gem::Requirement
155
160
  requirements:
156
161
  - - ">="
157
162
  - !ruby/object:Gem::Version
163
+ hash: 3
158
164
  segments:
159
165
  - 0
160
166
  version: "0"
161
167
  requirements:
162
168
  - Having Nokogiri installed will speed up XML handling when using the SOAP API.
163
169
  rubyforge_project:
164
- rubygems_version: 1.3.7
170
+ rubygems_version: 1.8.7
165
171
  signing_key:
166
172
  specification_version: 3
167
173
  summary: Integrate Adyen payment services in your Ruby on Rails application.