adyen 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/TODO CHANGED
@@ -1,11 +1,3 @@
1
- 1.0
2
- ===
3
-
4
- * Screen documentation
5
-
6
- Possible/Later
7
- ==============
8
-
9
1
  * Add a capture! method to an authorisation response from the PaymentService.
10
2
 
11
3
  * Add iDEAL API. To get started, see the `iDEAL' topic-branch which contains `ideal-scratchpad.rb' and a stub for a functional spec.
@@ -18,3 +10,7 @@ Possible/Later
18
10
  * adyen-soap
19
11
  * adyen-railtie
20
12
  * adyen-activemerchant
13
+
14
+ * Add mocks for notification requests in order to test your app
15
+
16
+ * Add more mock SOAP responses for testing your app.
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'adyen'
3
- s.version = "1.1.0"
4
- s.date = "2011-03-21"
3
+ s.version = "1.2.0"
4
+ s.date = "2011-05-12"
5
5
 
6
6
  s.summary = "Integrate Adyen payment services in your Ruby on Rails application."
7
7
  s.description = <<-EOS
@@ -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.1.0"
15
+ VERSION = "1.2.0"
16
16
 
17
17
  # @return [Configuration] The configuration singleton.
18
18
  def self.configuration
@@ -277,5 +277,46 @@ module Adyen
277
277
  :recurring_detail_reference => recurring_detail_reference
278
278
  }).disable
279
279
  end
280
+
281
+ # Stores and tokenises the creditcard details so that recurring payments can be made in the
282
+ # future.
283
+ #
284
+ # You do *not* have to include the card's CVC, because it won't be stored anyway.
285
+ #
286
+ # # @example
287
+ # response = Adyen::API.store_recurring_token(
288
+ # { :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
+ # )
291
+ # response.stored? # => true
292
+ #
293
+ # # Now we can authorize a payment with the token.
294
+ # authorize_response = Adyen::API.authorise_recurring_payment(
295
+ # invoice.id,
296
+ # { :currency => 'EUR', :value => invoice.amount },
297
+ # { :reference => user.id, :email => user.email, :ip => '8.8.8.8' },
298
+ # response.recurring_detail_reference
299
+ # )
300
+ # authorize_response.authorised? # => true
301
+ #
302
+ # @param [Hash] shopper A hash describing the shopper.
303
+ # @param [Hash] card A hash describing the creditcard details.
304
+ #
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.
308
+ #
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.
313
+ #
314
+ # @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
320
+ end
280
321
  end
281
322
  end
@@ -31,8 +31,21 @@ module Adyen
31
31
  call_webservice_action('disable', disable_request_body, DisableResponse)
32
32
  end
33
33
 
34
+ # @see API.store_recurring_token
35
+ def store_token
36
+ call_webservice_action('storeToken', store_token_request_body, StoreTokenResponse)
37
+ end
38
+
34
39
  private
35
40
 
41
+ # The card's CVC isn't needed when tokenising details, so insert `nil'.
42
+ def card_partial
43
+ validate_parameters!(:card => [:holder_name, :number, :expiry_year, :expiry_month])
44
+ card = @params[:card].values_at(:holder_name, :number, :cvc, :expiry_year)
45
+ card << @params[:card][:expiry_month].to_i
46
+ CARD_PARTIAL % card
47
+ end
48
+
36
49
  def list_request_body
37
50
  validate_parameters!(:merchant_account, :shopper => [:reference])
38
51
  LIST_LAYOUT % [@params[:merchant_account], @params[:shopper][:reference]]
@@ -46,6 +59,12 @@ module Adyen
46
59
  DISABLE_LAYOUT % [@params[:merchant_account], @params[:shopper][:reference], reference || '']
47
60
  end
48
61
 
62
+ def store_token_request_body
63
+ validate_parameters!(:merchant_account, :shopper => [:email, :reference])
64
+ content = card_partial
65
+ STORE_TOKEN_LAYOUT % [@params[:merchant_account], @params[:shopper][:reference], @params[:shopper][:email], content]
66
+ end
67
+
49
68
  class DisableResponse < Response
50
69
  DISABLED_RESPONSES = %w{ [detail-successfully-disabled] [all-details-successfully-disabled] }
51
70
 
@@ -121,6 +140,26 @@ module Adyen
121
140
  }
122
141
  end
123
142
  end
143
+
144
+ class StoreTokenResponse < Response
145
+ response_attrs :response, :recurring_detail_reference
146
+
147
+ def recurring_detail_reference
148
+ params[:recurring_detail_reference]
149
+ end
150
+
151
+ def success?
152
+ super && params[:response] == 'Success'
153
+ end
154
+
155
+ alias stored? success?
156
+
157
+ def params
158
+ @params ||= { :response => xml_querier.text('//recurring:storeTokenResponse/recurring:result/recurring:result'),
159
+ :reference => xml_querier.text('//recurring:storeTokenResponse/recurring:result/recurring:rechargeReference'),
160
+ :recurring_detail_reference => xml_querier.text('//recurring:storeTokenResponse/recurring:result/recurring:recurringDetailReference')}
161
+ end
162
+ end
124
163
  end
125
164
  end
126
165
  end
@@ -32,6 +32,16 @@ EOS
32
32
  end
33
33
  end
34
34
 
35
+ class ServerError < StandardError
36
+ def initialize(response, action, endpoint)
37
+ @response, @action, @endpoint = response, action, endpoint
38
+ end
39
+
40
+ def message
41
+ "[#{@response.code} #{@response.message}] A server error occurred while calling SOAP action `#{@action}' on endpoint `#{@endpoint}'."
42
+ end
43
+ end
44
+
35
45
  class << self
36
46
  # When a response instance has been assigned, the subsequent call to
37
47
  # {SimpleSOAPClient#call_webservice_action} will not make a remote call, but simply return
@@ -109,6 +119,7 @@ EOS
109
119
  request.start do |http|
110
120
  http_response = http.request(post)
111
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)
112
123
  response_class.new(http_response)
113
124
  end
114
125
  end
@@ -3,10 +3,10 @@ module Adyen
3
3
  class RecurringService < SimpleSOAPClient
4
4
  # @private
5
5
  LIST_LAYOUT = <<EOS
6
- <recurring:listRecurringDetails xmlns:recurring="http://recurring.services.adyen.com">
6
+ <recurring:listRecurringDetails xmlns:payment="http://payment.services.adyen.com" xmlns:recurring="http://recurring.services.adyen.com">
7
7
  <recurring:request>
8
8
  <recurring:recurring>
9
- <recurring:contract>RECURRING</recurring:contract>
9
+ <payment:contract>RECURRING</payment:contract>
10
10
  </recurring:recurring>
11
11
  <recurring:merchantAccount>%s</recurring:merchantAccount>
12
12
  <recurring:shopperReference>%s</recurring:shopperReference>
@@ -29,6 +29,31 @@ EOS
29
29
  RECURRING_DETAIL_PARTIAL = <<EOS
30
30
  <recurring:recurringDetailReference>%s</recurring:recurringDetailReference>
31
31
  EOS
32
+
33
+ STORE_TOKEN_LAYOUT = <<EOS
34
+ <recurring:storeToken xmlns:recurring="http://recurring.services.adyen.com" xmlns:payment="http://payment.services.adyen.com">
35
+ <recurring:request>
36
+ <recurring:recurring>
37
+ <payment:contract>RECURRING</payment:contract>
38
+ </recurring:recurring>
39
+ <recurring:merchantAccount>%s</recurring:merchantAccount>
40
+ <recurring:shopperReference>%s</recurring:shopperReference>
41
+ <recurring:shopperEmail>%s</recurring:shopperEmail>
42
+ %s
43
+ </recurring:request>
44
+ </recurring:storeToken>
45
+ EOS
46
+
47
+ # @private
48
+ CARD_PARTIAL = <<EOS
49
+ <recurring:card>
50
+ <payment:holderName>%s</payment:holderName>
51
+ <payment:number>%s</payment:number>
52
+ <payment:cvc>%s</payment:cvc>
53
+ <payment:expiryYear>%s</payment:expiryYear>
54
+ <payment:expiryMonth>%02d</payment:expiryMonth>
55
+ </recurring:card>
56
+ EOS
32
57
  end
33
58
  end
34
59
  end
@@ -78,6 +78,17 @@ class Adyen::Configuration
78
78
  #
79
79
  # @return [Hash]
80
80
  attr_accessor :default_form_params
81
+
82
+ # Username that's set in Notification settings screen in Adyen PSP system and used by notification service to
83
+ # authenticate instant payment notification requests.
84
+ #
85
+ # @return [String]
86
+ attr_accessor :ipn_username
87
+
88
+ # Password used to authenticate notification requests together with '+ipn_username+' configuration attribute.
89
+ #
90
+ # @return [String]
91
+ attr_accessor :ipn_password
81
92
 
82
93
  ######################################################
83
94
  # SKINS
@@ -126,7 +126,7 @@ module Adyen
126
126
  # @param [Hash] parameters The payment parameters to include in the payment request.
127
127
  # @return [String] An absolute URL to redirect to the Adyen payment system.
128
128
  def redirect_url(parameters = {})
129
- url + '?' + payment_parameters(parameters).map { |(k, v)|
129
+ url + '?' + payment_parameters(parameters).map{|k,v| [k.to_s,v] }.sort.map { |(k, v)|
130
130
  "#{camelize(k)}=#{CGI.escape(v.to_s)}" }.join('&')
131
131
  end
132
132
 
@@ -171,13 +171,14 @@ module Adyen
171
171
  # @return [String] The string for which the siganture is calculated.
172
172
  def calculate_signature_string(parameters)
173
173
  merchant_sig_string = ""
174
- merchant_sig_string << parameters[:payment_amount].to_s << parameters[:currency_code].to_s <<
175
- parameters[:ship_before_date].to_s << parameters[:merchant_reference].to_s <<
176
- parameters[:skin_code].to_s << parameters[:merchant_account].to_s <<
177
- parameters[:session_validity].to_s << parameters[:shopper_email].to_s <<
178
- parameters[:shopper_reference].to_s << parameters[:recurring_contract].to_s <<
179
- parameters[:allowed_methods].to_s << parameters[:blocked_methods].to_s <<
180
- parameters[:shopper_statement].to_s << parameters[:billing_address_type].to_s
174
+ merchant_sig_string << parameters[:payment_amount].to_s << parameters[:currency_code].to_s <<
175
+ parameters[:ship_before_date].to_s << parameters[:merchant_reference].to_s <<
176
+ parameters[:skin_code].to_s << parameters[:merchant_account].to_s <<
177
+ parameters[:session_validity].to_s << parameters[:shopper_email].to_s <<
178
+ parameters[:shopper_reference].to_s << parameters[:recurring_contract].to_s <<
179
+ parameters[:allowed_methods].to_s << parameters[:blocked_methods].to_s <<
180
+ parameters[:shopper_statement].to_s << parameters[:merchant_return_data].to_s <<
181
+ parameters[:billing_address_type].to_s << parameters[:offset].to_s
181
182
  end
182
183
 
183
184
  # Calculates the payment request signature for the given payment parameters.
@@ -206,7 +207,8 @@ module Adyen
206
207
  # @param [Hash] params A hash of HTTP GET parameters for the redirect request.
207
208
  # @return [String] The signature string.
208
209
  def redirect_signature_string(params)
209
- params[:authResult].to_s + params[:pspReference].to_s + params[:merchantReference].to_s + params[:skinCode].to_s
210
+ params[:authResult].to_s + params[:pspReference].to_s + params[:merchantReference].to_s +
211
+ params[:skinCode].to_s + params[:merchantReturnData].to_s
210
212
  end
211
213
 
212
214
  # Computes the redirect signature using the request parameters, so that the
@@ -120,6 +120,17 @@ describe Adyen::API do
120
120
  @recurring.should_receive(method)
121
121
  end
122
122
 
123
+ it "performs a `tokenize creditcard details' request" do
124
+ should_map_shortcut_to(:store_token,
125
+ :shopper => { :reference => 'user-id', :email => 's.hopper@example.com' },
126
+ :card => { :expiry_month => 12, :expiry_year => 2012, :holder_name => "Simon Hopper", :number => '4444333322221111' }
127
+ )
128
+ Adyen::API.store_recurring_token(
129
+ { :reference => 'user-id', :email => 's.hopper@example.com' },
130
+ { :expiry_month => 12, :expiry_year => 2012, :holder_name => "Simon Hopper", :number => '4444333322221111' }
131
+ )
132
+ end
133
+
123
134
  it "preforms a `list recurring details' request" do
124
135
  should_map_shortcut_to(:list, :shopper => { :reference => 'user-id' })
125
136
  Adyen::API.list_recurring_details('user-id')
@@ -6,7 +6,29 @@ describe Adyen::API::RecurringService do
6
6
  include APISpecHelper
7
7
 
8
8
  before do
9
- @params = { :shopper => { :reference => 'user-id' } }
9
+ @params = {
10
+ :reference => 'order-id',
11
+ :amount => {
12
+ :currency => 'EUR',
13
+ :value => '1234',
14
+ },
15
+ :shopper => {
16
+ :email => 's.hopper@example.com',
17
+ :reference => 'user-id',
18
+ :ip => '61.294.12.12',
19
+ },
20
+ :card => {
21
+ :expiry_month => 12,
22
+ :expiry_year => 2012,
23
+ :holder_name => 'Simon わくわく Hopper',
24
+ :number => '4444333322221111',
25
+ :cvc => '737',
26
+ # Maestro UK/Solo only
27
+ #:issue_number => ,
28
+ #:start_month => ,
29
+ #:start_year => ,
30
+ }
31
+ }
10
32
  @recurring = @object = Adyen::API::RecurringService.new(@params)
11
33
  end
12
34
 
@@ -23,7 +45,7 @@ describe Adyen::API::RecurringService do
23
45
  end
24
46
 
25
47
  it "includes the type of contract, which is always `RECURRING'" do
26
- text('./recurring:recurring/recurring:contract').should == 'RECURRING'
48
+ text('./recurring:recurring/payment:contract').should == 'RECURRING'
27
49
  end
28
50
  end
29
51
 
@@ -102,4 +124,55 @@ describe Adyen::API::RecurringService do
102
124
 
103
125
  it_should_return_params_for_each_xml_backend(:response => '[detail-successfully-disabled]')
104
126
  end
127
+
128
+ describe_request_body_of :store_token, '//recurring:storeToken/recurring:request' do
129
+ it_should_validate_request_parameters :merchant_account,
130
+ :shopper => [:email, :reference]
131
+
132
+ it "includes the merchant account handle" do
133
+ text('./recurring:merchantAccount').should == 'SuperShopper'
134
+ end
135
+
136
+ it "includes the shopper’s reference" do
137
+ text('./recurring:shopperReference').should == 'user-id'
138
+ end
139
+
140
+ it "includes the shopper’s email" do
141
+ text('./recurring:shopperEmail').should == 's.hopper@example.com'
142
+ end
143
+
144
+ it "includes the creditcard details" do
145
+ xpath('./recurring:card') do |card|
146
+ # there's no reason why Nokogiri should escape these characters, but as long as they're correct
147
+ card.text('./payment:holderName').should == 'Simon &#x308F;&#x304F;&#x308F;&#x304F; Hopper'
148
+ card.text('./payment:number').should == '4444333322221111'
149
+ card.text('./payment:cvc').should == '737'
150
+ card.text('./payment:expiryMonth').should == '12'
151
+ card.text('./payment:expiryYear').should == '2012'
152
+ end
153
+ end
154
+
155
+ it "formats the creditcard’s expiry month as a two digit number" do
156
+ @recurring.params[:card][:expiry_month] = 6
157
+ text('./recurring:card/payment:expiryMonth').should == '06'
158
+ end
159
+
160
+ it "includes the necessary recurring and one-click contract info if the `:recurring' param is truthful" do
161
+ text('./recurring:recurring/payment:contract').should == 'RECURRING'
162
+ end
163
+ end
164
+
165
+ describe_response_from :disable, (DISABLE_RESPONSE % '[detail-successfully-disabled]'), 'disable' do
166
+ it "returns whether or not it was disabled" do
167
+ @response.should be_success
168
+ @response.should be_disabled
169
+
170
+ stub_net_http(DISABLE_RESPONSE % '[all-details-successfully-disabled]')
171
+ @response = @recurring.disable
172
+ @response.should be_success
173
+ @response.should be_disabled
174
+ end
175
+
176
+ it_should_return_params_for_each_xml_backend(:response => '[detail-successfully-disabled]')
177
+ end
105
178
  end
@@ -1,5 +1,6 @@
1
1
  # encoding: UTF-8
2
2
 
3
+ require 'date'
3
4
  require 'spec_helper'
4
5
  require 'adyen/form'
5
6
 
@@ -63,6 +64,8 @@ describe Adyen::Form do
63
64
 
64
65
  it "should calculate the signature string correctly" do
65
66
  Adyen::Form.redirect_signature_string(@params).should == 'AUTHORISED1211992213193029Internet Order 123454aD37dJA'
67
+ params = @params.merge(:merchantReturnData => 'testing1234')
68
+ Adyen::Form.redirect_signature_string(params).should == 'AUTHORISED1211992213193029Internet Order 123454aD37dJAtesting1234'
66
69
  end
67
70
 
68
71
  it "should calculate the signature correctly" do
@@ -142,9 +145,13 @@ describe Adyen::Form do
142
145
  Adyen::Form.do_parameter_transformations!(@parameters)
143
146
  end
144
147
 
145
- it "should construct the signature string correctly" do
148
+ it "should construct the signature base string correctly" do
146
149
  signature_string = Adyen::Form.calculate_signature_string(@parameters)
147
150
  signature_string.should == "10000GBP2007-10-20Internet Order 123454aD37dJATestMerchant2007-10-11T11:00:00Z"
151
+
152
+ signature_string = Adyen::Form.calculate_signature_string(@parameters.merge(:merchant_return_data => 'testing123'))
153
+ signature_string.should == "10000GBP2007-10-20Internet Order 123454aD37dJATestMerchant2007-10-11T11:00:00Ztesting123"
154
+
148
155
  end
149
156
 
150
157
  it "should calculate the signature correctly" do
@@ -152,7 +159,7 @@ describe Adyen::Form do
152
159
  signature.should == 'x58ZcRVL1H6y+XSeBGrySJ9ACVo='
153
160
  end
154
161
 
155
- it "should calculate the signature correctly for a recurring payment" do
162
+ it "should calculate the signature base string correctly for a recurring payment" do
156
163
  # Add the required recurrent payment attributes
157
164
  @parameters.merge!(:recurring_contract => 'DEFAULT', :shopper_reference => 'grasshopper52', :shopper_email => 'gras.shopper@somewhere.org')
158
165
 
@@ -1,7 +1,5 @@
1
1
  # encoding: UTF-8
2
- require 'spec_helper'
3
-
4
- require 'rubygems'
2
+ require 'api/spec_helper'
5
3
  require 'nokogiri'
6
4
 
7
5
  API_SPEC_INITIALIZER = File.expand_path("../initializer.rb", __FILE__)
@@ -9,6 +7,7 @@ API_SPEC_INITIALIZER = File.expand_path("../initializer.rb", __FILE__)
9
7
  if File.exist?(API_SPEC_INITIALIZER)
10
8
 
11
9
  describe Adyen::API, "with an actual remote connection" do
10
+
12
11
  before :all do
13
12
  require API_SPEC_INITIALIZER
14
13
  Net::HTTP.stubbing_enabled = false
@@ -58,6 +57,15 @@ if File.exist?(API_SPEC_INITIALIZER)
58
57
  response.psp_reference.should_not be_empty
59
58
  end
60
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
68
+
61
69
  it "captures a payment" do
62
70
  response = Adyen::API.capture_payment(@payment_response.psp_reference, { :currency => 'EUR', :value => '1234' })
63
71
  response.should be_success
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 1
7
- - 1
7
+ - 2
8
8
  - 0
9
- version: 1.1.0
9
+ version: 1.2.0
10
10
  platform: ruby
11
11
  authors:
12
12
  - Willem van Bergen
@@ -17,7 +17,7 @@ autorequire:
17
17
  bindir: bin
18
18
  cert_chain: []
19
19
 
20
- date: 2011-03-21 00:00:00 -04:00
20
+ date: 2011-05-12 00:00:00 -04:00
21
21
  default_executable:
22
22
  dependencies:
23
23
  - !ruby/object:Gem::Dependency