adyen 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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