fingertips-adyen 0.3.7.20100917

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,151 @@
1
+ require 'active_record'
2
+
3
+ module Adyen
4
+
5
+ # The +Adyen::Notification+ class handles notifications sent by Adyen to your servers.
6
+ #
7
+ # Because notifications contain important payment status information, you should store
8
+ # these notifications in your database. For this reason, +Adyen::Notification+ inherits
9
+ # from +ActiveRecord::Base+, and a migration is included to simply create a suitable table
10
+ # to store the notifications in.
11
+ #
12
+ # Adyen can either send notifications to you via HTTP POST requests, or SOAP requests.
13
+ # Because SOAP is not really well supported in Rails and setting up a SOAP server is
14
+ # not trivial, only handling HTTP POST notifications is currently supported.
15
+ #
16
+ # @example
17
+ # @notification = Adyen::Notification::HttpPost.log(request)
18
+ # if @notification.successful_authorisation?
19
+ # @invoice = Invoice.find(@notification.merchant_reference)
20
+ # @invoice.set_paid!
21
+ # end
22
+ #
23
+ # @see Adyen::Notification::HttpPost.log
24
+ class Notification < ActiveRecord::Base
25
+
26
+ # The default table name to use for the notifications table.
27
+ DEFAULT_TABLE_NAME = :adyen_notifications
28
+ set_table_name(DEFAULT_TABLE_NAME)
29
+
30
+ # A notification should always include an event_code
31
+ validates_presence_of :event_code
32
+
33
+ # A notification should always include a psp_reference
34
+ validates_presence_of :psp_reference
35
+
36
+ # A notification should be unique using the composed key of
37
+ # [:psp_reference, :event_code, :success]
38
+ validates_uniqueness_of :success, :scope => [:psp_reference, :event_code]
39
+
40
+ # Make sure we don't end up with an original_reference with an empty string
41
+ before_validation { |notification| notification.original_reference = nil if notification.original_reference.blank? }
42
+
43
+ # Logs an incoming notification into the database.
44
+ #
45
+ # @param [Hash] params The notification parameters that should be stored in the database.
46
+ # @return [Adyen::Notification] The initiated and persisted notification instance.
47
+ # @raise This method will raise an exception if the notification cannot be stored.
48
+ # @see Adyen::Notification::HttpPost.log
49
+ def self.log(params)
50
+ converted_params = {}
51
+ # Convert each attribute from CamelCase notation to under_score notation
52
+ # For example, merchantReference will be converted to merchant_reference
53
+ params.each do |key, value|
54
+ field_name = key.to_s.underscore
55
+ converted_params[field_name] = value if self.column_names.include?(field_name)
56
+ end
57
+ self.create!(converted_params)
58
+ end
59
+
60
+ # Returns true if this notification is an AUTHORISATION notification
61
+ # @return [true, false] true iff event_code == 'AUTHORISATION'
62
+ # @see Adyen.notification#successful_authorisation?
63
+ def authorisation?
64
+ event_code == 'AUTHORISATION'
65
+ end
66
+
67
+ alias :authorization? :authorisation?
68
+
69
+ # Returns true if this notification is an AUTHORISATION notification and
70
+ # the success status indicates that the authorization was successfull.
71
+ # @return [true, false] true iff the notification is an authorization
72
+ # and the authorization was successful according to the success field.
73
+ def successful_authorisation?
74
+ event_code == 'AUTHORISATION' && success?
75
+ end
76
+
77
+ alias :successful_authorization? :successful_authorisation?
78
+
79
+ # Collect a payment using the recurring contract that was initiated with
80
+ # this notification. The payment is collected using a SOAP call to the
81
+ # Adyen SOAP service for recurring payments.
82
+ # @param [Hash] options The payment parameters.
83
+ # @see Adyen::SOAP::RecurringService#submit
84
+ def collect_payment_for_recurring_contract!(options)
85
+ # Make sure we convert the value to cents
86
+ options[:value] = Adyen::Formatter::Price.in_cents(options[:value])
87
+ raise "This is not a recurring contract!" unless event_code == 'RECURRING_CONTRACT'
88
+ Adyen::SOAP::RecurringService.submit(options.merge(:recurring_reference => self.psp_reference))
89
+ end
90
+
91
+ # Deactivates the recurring contract that was initiated with this notification.
92
+ # The contract is deactivated by sending a SOAP call to the Adyen SOAP service for
93
+ # recurring contracts.
94
+ # @param [Hash] options The recurring contract parameters.
95
+ # @see Adyen::SOAP::RecurringService#deactivate
96
+ def deactivate_recurring_contract!(options)
97
+ raise "This is not a recurring contract!" unless event_code == 'RECURRING_CONTRACT'
98
+ Adyen::SOAP::RecurringService.deactivate(options.merge(:recurring_reference => self.psp_reference))
99
+ end
100
+
101
+ class HttpPost < Notification
102
+
103
+ def self.log(request)
104
+ super(request.params)
105
+ end
106
+
107
+ def live=(value)
108
+ self.write_attribute(:live, [true, 1, '1', 'true'].include?(value))
109
+ end
110
+
111
+ def success=(value)
112
+ self.write_attribute(:success, [true, 1, '1', 'true'].include?(value))
113
+ end
114
+
115
+ def value=(value)
116
+ self.write_attribute(:value, Adyen::Formatter::Price.from_cents(value)) unless value.blank?
117
+ end
118
+ end
119
+
120
+ # An ActiveRecord migration that can be used to create a suitable table
121
+ # to store Adyen::Notification instances for your application.
122
+ class Migration < ActiveRecord::Migration
123
+
124
+ def self.up(table_name = Adyen::Notification::DEFAULT_TABLE_NAME)
125
+ create_table(table_name) do |t|
126
+ t.boolean :live, :null => false, :default => false
127
+ t.string :event_code, :null => false
128
+ t.string :psp_reference, :null => false
129
+ t.string :original_reference, :null => true
130
+ t.string :merchant_reference, :null => false
131
+ t.string :merchant_account_code, :null => false
132
+ t.datetime :event_date, :null => false
133
+ t.boolean :success, :null => false, :default => false
134
+ t.string :payment_method, :null => true
135
+ t.string :operations, :null => true
136
+ t.text :reason
137
+ t.string :currency, :null => false, :limit => 3
138
+ t.decimal :value, :null => true, :precision => 9, :scale => 2
139
+ t.boolean :processed, :null => false, :default => false
140
+ t.timestamps
141
+ end
142
+ add_index table_name, [:psp_reference, :event_code, :success], :unique => true, :name => 'adyen_notification_uniqueness'
143
+ end
144
+
145
+ def self.down(table_name = Adyen::Notification::DEFAULT_TABLE_NAME)
146
+ remove_index(table_name, :name => 'adyen_notification_uniqueness')
147
+ drop_table(table_name)
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,86 @@
1
+ require "#{File.dirname(__FILE__)}/spec_helper.rb"
2
+
3
+ describe Adyen do
4
+
5
+ describe '.load_config' do
6
+
7
+ it "should set the environment correctly from the gonfiguration" do
8
+ Adyen.load_config(:environment => 'from_config')
9
+ Adyen.environment.should == 'from_config'
10
+ end
11
+
12
+ it "should recursively set settings for submodules" do
13
+ Adyen.load_config(:SOAP => { :username => 'foo', :password => 'bar' },
14
+ :Form => { :default_parameters => { :merchant_account => 'us' }})
15
+ Adyen::SOAP.username.should == 'foo'
16
+ Adyen::SOAP.password.should == 'bar'
17
+ Adyen::Form.default_parameters.should == { :merchant_account => 'us' }
18
+ end
19
+
20
+ it "should raise an error when using a non-existing module" do
21
+ lambda { Adyen.load_config(:Unknown => { :a => 'b' }) }.should raise_error
22
+ end
23
+
24
+ it "should raise an error when using a non-existing setting" do
25
+ lambda { Adyen.load_config(:blah => 1234) }.should raise_error
26
+ end
27
+
28
+ it "should set skins from a hash configuration" do
29
+ Adyen.load_config(:Form => {:skins => {
30
+ :first => { :skin_code => '1234', :shared_secret => 'abcd' },
31
+ :second => { :skin_code => '5678', :shared_secret => 'efgh' }}})
32
+
33
+ Adyen::Form.skins.should == {
34
+ :first => {:skin_code => "1234", :name => :first, :shared_secret => "abcd" },
35
+ :second => {:skin_code => "5678", :name => :second, :shared_secret => "efgh" }}
36
+ end
37
+ end
38
+
39
+ describe Adyen::Encoding do
40
+ it "should a hmac_base64 correcly" do
41
+ encoded_str = Adyen::Encoding.hmac_base64('bla', 'bla')
42
+ encoded_str.should == '6nItEkVpIYF+i1RwrEyQ7RHmrfU='
43
+ end
44
+
45
+ it "should gzip_base64 correcly" do
46
+ encoded_str = Adyen::Encoding.gzip_base64('bla')
47
+ encoded_str.length.should == 32
48
+ end
49
+ end
50
+
51
+ describe Adyen::Formatter::DateTime do
52
+ it "should accept dates" do
53
+ Adyen::Formatter::DateTime.fmt_date(Date.today).should match(/^\d{4}-\d{2}-\d{2}$/)
54
+ end
55
+
56
+ it "should accept times" do
57
+ Adyen::Formatter::DateTime.fmt_time(Time.now).should match(/^\d{4}-\d{2}-\d{2}T\d{2}\:\d{2}\:\d{2}Z$/)
58
+ end
59
+
60
+ it "should accept valid time strings" do
61
+ Adyen::Formatter::DateTime.fmt_time('2009-01-01T11:11:11Z').should match(/^\d{4}-\d{2}-\d{2}T\d{2}\:\d{2}\:\d{2}Z$/)
62
+ end
63
+
64
+ it "should accept valid time strings" do
65
+ Adyen::Formatter::DateTime.fmt_date('2009-01-01').should match(/^\d{4}-\d{2}-\d{2}$/)
66
+ end
67
+
68
+ it "should raise on an invalid time string" do
69
+ lambda { Adyen::Formatter::DateTime.fmt_time('2009-01-01 11:11:11') }.should raise_error
70
+ end
71
+
72
+ it "should raise on an invalid date string" do
73
+ lambda { Adyen::Formatter::DateTime.fmt_date('2009-1-1') }.should raise_error
74
+ end
75
+ end
76
+
77
+ describe Adyen::Formatter::Price do
78
+ it "should return a Fixnum with digits only when converting to cents" do
79
+ Adyen::Formatter::Price.in_cents(33.76).should be_kind_of(Fixnum)
80
+ end
81
+
82
+ it "should return a BigDecimal when converting from cents" do
83
+ Adyen::Formatter::Price.from_cents(1234).should be_kind_of(BigDecimal)
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,562 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+ require 'adyen/api'
3
+
4
+ require 'rubygems'
5
+ require 'nokogiri'
6
+ require 'rexml/document'
7
+
8
+ module Net
9
+ class HTTP
10
+ class Post
11
+ attr_reader :header
12
+ attr_reader :assigned_basic_auth
13
+
14
+ alias old_basic_auth basic_auth
15
+ def basic_auth(username, password)
16
+ if Net::HTTP.stubbing_enabled
17
+ @assigned_basic_auth = [username, password]
18
+ else
19
+ old_basic_auth
20
+ end
21
+ end
22
+
23
+ def soap_action
24
+ header['soapaction'].first
25
+ end
26
+ end
27
+
28
+ class << self
29
+ attr_accessor :stubbing_enabled, :posted, :stubbed_response
30
+
31
+ def stubbing_enabled=(enabled)
32
+ reset! if @stubbing_enabled = enabled
33
+ end
34
+
35
+ def reset!
36
+ @posted = nil
37
+ @stubbed_response = nil
38
+ end
39
+ end
40
+
41
+ def host
42
+ @address
43
+ end
44
+
45
+ alias old_start start
46
+ def start
47
+ Net::HTTP.stubbing_enabled ? yield(self) : old_start
48
+ end
49
+
50
+ alias old_request request
51
+ def request(request)
52
+ if Net::HTTP.stubbing_enabled
53
+ self.class.posted = [self, request]
54
+ self.class.stubbed_response
55
+ else
56
+ old_request(request)
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ module Adyen
63
+ module API
64
+ class PaymentService
65
+ public :authorise_payment_request_body, :authorise_recurring_payment_request_body
66
+ end
67
+
68
+ class RecurringService
69
+ public :list_request_body
70
+ end
71
+ end
72
+ end
73
+
74
+ module APISpecHelper
75
+ def node_for_current_method(object)
76
+ node = Adyen::API::XMLQuerier.new(object.send(@method))
77
+ end
78
+
79
+ def xpath(query, &block)
80
+ node_for_current_method.xpath(query, &block)
81
+ end
82
+
83
+ def text(query)
84
+ node_for_current_method.text(query)
85
+ end
86
+
87
+ def stub_net_http(response_body)
88
+ Net::HTTP.stubbing_enabled = true
89
+ response = Net::HTTPOK.new('1.1', '200', 'OK')
90
+ response.stub!(:body).and_return(response_body)
91
+ Net::HTTP.stubbed_response = response
92
+ end
93
+
94
+ def self.included(klass)
95
+ klass.extend ClassMethods
96
+ end
97
+
98
+ module ClassMethods
99
+ def for_each_xml_backend(&block)
100
+ [:nokogiri, :rexml].each do |xml_backend|
101
+ describe "with a #{xml_backend} backend" do
102
+ before { Adyen::API::XMLQuerier.backend = xml_backend }
103
+ after { Adyen::API::XMLQuerier.backend = :nokogiri }
104
+ instance_eval(&block)
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ class SOAPClient < Adyen::API::SimpleSOAPClient
111
+ ENDPOINT_URI = 'https://%s.example.com/soap/Action'
112
+ end
113
+ end
114
+
115
+ shared_examples_for "payment requests" do
116
+ it "includes the merchant account handle" do
117
+ text('./payment:merchantAccount').should == 'SuperShopper'
118
+ end
119
+
120
+ it "includes the payment reference of the merchant" do
121
+ text('./payment:reference').should == 'order-id'
122
+ end
123
+
124
+ it "includes the given amount of `currency'" do
125
+ xpath('./payment:amount') do |amount|
126
+ amount.text('./common:currency').should == 'EUR'
127
+ amount.text('./common:value').should == '1234'
128
+ end
129
+ end
130
+
131
+ it "includes the shopper’s details" do
132
+ text('./payment:shopperReference').should == 'user-id'
133
+ text('./payment:shopperEmail').should == 's.hopper@example.com'
134
+ text('./payment:shopperIP').should == '61.294.12.12'
135
+ end
136
+
137
+ it "only includes shopper details for given parameters" do
138
+ @payment.params[:shopper].delete(:reference)
139
+ xpath('./payment:shopperReference').should be_empty
140
+ @payment.params[:shopper].delete(:email)
141
+ xpath('./payment:shopperEmail').should be_empty
142
+ @payment.params[:shopper].delete(:ip)
143
+ xpath('./payment:shopperIP').should be_empty
144
+ end
145
+
146
+ it "does not include any shopper details if none are given" do
147
+ @payment.params.delete(:shopper)
148
+ xpath('./payment:shopperReference').should be_empty
149
+ xpath('./payment:shopperEmail').should be_empty
150
+ xpath('./payment:shopperIP').should be_empty
151
+ end
152
+ end
153
+
154
+ describe Adyen::API do
155
+ include APISpecHelper
156
+
157
+ before :all do
158
+ Adyen::API.default_params = { :merchant_account => 'SuperShopper' }
159
+ Adyen::API.username = 'SuperShopper'
160
+ Adyen::API.password = 'secret'
161
+ end
162
+
163
+ describe Adyen::API::SimpleSOAPClient do
164
+ before do
165
+ @client = APISpecHelper::SOAPClient.new(:reference => 'order-id')
166
+ end
167
+
168
+ it "returns the endpoint, for the current environment, from the ENDPOINT_URI constant" do
169
+ uri = APISpecHelper::SOAPClient.endpoint
170
+ uri.scheme.should == 'https'
171
+ uri.host.should == 'test.example.com'
172
+ uri.path.should == '/soap/Action'
173
+ end
174
+
175
+ it "initializes with the given parameters" do
176
+ @client.params[:reference].should == 'order-id'
177
+ end
178
+
179
+ it "merges the default parameters with the given ones" do
180
+ @client.params[:merchant_account].should == 'SuperShopper'
181
+ end
182
+
183
+ describe "call_webservice_action" do
184
+ before do
185
+ stub_net_http(AUTHORISE_RESPONSE)
186
+ @client.call_webservice_action('Action', '<bananas>Yes, please</bananas>')
187
+ @request, @post = Net::HTTP.posted
188
+ end
189
+
190
+ after do
191
+ Net::HTTP.stubbing_enabled = false
192
+ end
193
+
194
+ it "posts to the class's endpoint" do
195
+ endpoint = APISpecHelper::SOAPClient.endpoint
196
+ @request.host.should == endpoint.host
197
+ @request.port.should == endpoint.port
198
+ @post.path.should == endpoint.path
199
+ end
200
+
201
+ it "makes a request over SSL" do
202
+ @request.use_ssl.should == true
203
+ end
204
+
205
+ it "verifies certificates" do
206
+ File.should exist(Adyen::API::SimpleSOAPClient::CACERT)
207
+ @request.ca_file.should == Adyen::API::SimpleSOAPClient::CACERT
208
+ @request.verify_mode.should == OpenSSL::SSL::VERIFY_PEER
209
+ end
210
+
211
+ it "uses basic-authentication with the credentials set on the Adyen::API module" do
212
+ username, password = @post.assigned_basic_auth
213
+ username.should == 'SuperShopper'
214
+ password.should == 'secret'
215
+ end
216
+
217
+ it "sends the proper headers" do
218
+ @post.header.should == {
219
+ 'accept' => ['text/xml'],
220
+ 'content-type' => ['text/xml; charset=utf-8'],
221
+ 'soapaction' => ['Action']
222
+ }
223
+ end
224
+ end
225
+ end
226
+
227
+ describe "shortcut methods" do
228
+ it "performs a `authorise payment' request" do
229
+ payment = mock('PaymentService')
230
+ Adyen::API::PaymentService.should_receive(:new).with(:reference => 'order-id').and_return(payment)
231
+ payment.should_receive(:authorise_payment)
232
+ Adyen::API.authorise_payment(:reference => 'order-id')
233
+ end
234
+
235
+ it "performs a `authorise recurring payment' request" do
236
+ payment = mock('PaymentService')
237
+ Adyen::API::PaymentService.should_receive(:new).with(:reference => 'order-id').and_return(payment)
238
+ payment.should_receive(:authorise_recurring_payment)
239
+ Adyen::API.authorise_recurring_payment(:reference => 'order-id')
240
+ end
241
+ end
242
+
243
+ describe Adyen::API::PaymentService do
244
+ describe "for a normal payment request" do
245
+ before do
246
+ @params = {
247
+ :reference => 'order-id',
248
+ :amount => {
249
+ :currency => 'EUR',
250
+ :value => '1234',
251
+ },
252
+ :shopper => {
253
+ :email => 's.hopper@example.com',
254
+ :reference => 'user-id',
255
+ :ip => '61.294.12.12',
256
+ },
257
+ :card => {
258
+ :expiry_month => 12,
259
+ :expiry_year => 2012,
260
+ :holder_name => 'Simon わくわく Hopper',
261
+ :number => '4444333322221111',
262
+ :cvc => '737',
263
+ # Maestro UK/Solo only
264
+ #:issue_number => ,
265
+ #:start_month => ,
266
+ #:start_year => ,
267
+ }
268
+ }
269
+ @payment = Adyen::API::PaymentService.new(@params)
270
+ end
271
+
272
+ describe "authorise_payment_request_body" do
273
+ before :all do
274
+ @method = :authorise_payment_request_body
275
+ end
276
+
277
+ it_should_behave_like "payment requests"
278
+
279
+ it "includes the creditcard details" do
280
+ xpath('./payment:card') do |card|
281
+ # there's no reason why Nokogiri should escape these characters, but as long as they're correct
282
+ card.text('./payment:holderName').should == 'Simon &#x308F;&#x304F;&#x308F;&#x304F; Hopper'
283
+ card.text('./payment:number').should == '4444333322221111'
284
+ card.text('./payment:cvc').should == '737'
285
+ card.text('./payment:expiryMonth').should == '12'
286
+ card.text('./payment:expiryYear').should == '2012'
287
+ end
288
+ end
289
+
290
+ it "formats the creditcard’s expiry month as a two digit number" do
291
+ @payment.params[:card][:expiry_month] = 6
292
+ text('./payment:card/payment:expiryMonth').should == '06'
293
+ end
294
+
295
+ it "includes the necessary recurring contract info if the `:recurring' param is truthful" do
296
+ xpath('./recurring:recurring/payment:contract').should be_empty
297
+ @payment.params[:recurring] = true
298
+ text('./recurring:recurring/payment:contract').should == 'RECURRING'
299
+ end
300
+ end
301
+
302
+ describe "authorise_payment" do
303
+ before do
304
+ stub_net_http(AUTHORISE_RESPONSE)
305
+ @payment.authorise_payment
306
+ @request, @post = Net::HTTP.posted
307
+ end
308
+
309
+ after do
310
+ Net::HTTP.stubbing_enabled = false
311
+ end
312
+
313
+ it "posts the body generated for the given parameters" do
314
+ @post.body.should == @payment.authorise_payment_request_body
315
+ end
316
+
317
+ it "posts to the correct SOAP action" do
318
+ @post.soap_action.should == 'authorise'
319
+ end
320
+
321
+ for_each_xml_backend do
322
+ it "returns a hash with parsed response details" do
323
+ @payment.authorise_payment.should == {
324
+ :psp_reference => '9876543210987654',
325
+ :result_code => 'Authorised',
326
+ :auth_code => '1234',
327
+ :refusal_reason => ''
328
+ }
329
+ end
330
+ end
331
+ end
332
+
333
+ describe "authorise_recurring_payment_request_body" do
334
+ before :all do
335
+ @method = :authorise_recurring_payment_request_body
336
+ end
337
+
338
+ it_should_behave_like "payment requests"
339
+
340
+ it "does not include any creditcard details" do
341
+ xpath('./payment:card').should be_empty
342
+ end
343
+
344
+ it "includes the contract type, which is always `RECURRING'" do
345
+ text('./recurring:recurring/payment:contract').should == 'RECURRING'
346
+ end
347
+
348
+ it "obviously includes the obligatory self-‘describing’ nonsense parameters" do
349
+ text('./payment:shopperInteraction').should == 'ContAuth'
350
+ end
351
+
352
+ it "uses the latest recurring detail reference, by default" do
353
+ text('./payment:selectedRecurringDetailReference').should == 'LATEST'
354
+ end
355
+
356
+ it "uses the given recurring detail reference" do
357
+ @payment.params[:recurring_detail_reference] = 'RecurringDetailReference1'
358
+ text('./payment:selectedRecurringDetailReference').should == 'RecurringDetailReference1'
359
+ end
360
+ end
361
+
362
+ describe "authorise_recurring_payment" do
363
+ before do
364
+ stub_net_http(AUTHORISE_RESPONSE)
365
+ @payment.authorise_recurring_payment
366
+ @request, @post = Net::HTTP.posted
367
+ end
368
+
369
+ after do
370
+ Net::HTTP.stubbing_enabled = false
371
+ end
372
+
373
+ it "posts the body generated for the given parameters" do
374
+ @post.body.should == @payment.authorise_recurring_payment_request_body
375
+ end
376
+
377
+ it "posts to the correct SOAP action" do
378
+ @post.soap_action.should == 'authorise'
379
+ end
380
+
381
+ for_each_xml_backend do
382
+ it "returns a hash with parsed response details" do
383
+ @payment.authorise_recurring_payment.should == {
384
+ :psp_reference => '9876543210987654',
385
+ :result_code => 'Authorised',
386
+ :auth_code => '1234',
387
+ :refusal_reason => ''
388
+ }
389
+ end
390
+ end
391
+ end
392
+ end
393
+
394
+ private
395
+
396
+ def node_for_current_method
397
+ super(@payment).xpath('//payment:authorise/payment:paymentRequest')
398
+ end
399
+ end
400
+
401
+ describe Adyen::API::RecurringService do
402
+ before do
403
+ @params = { :shopper => { :reference => 'user-id' } }
404
+ @recurring = Adyen::API::RecurringService.new(@params)
405
+ end
406
+
407
+ describe "list_request_body" do
408
+ before :all do
409
+ @method = :list_request_body
410
+ end
411
+
412
+ it "includes the merchant account handle" do
413
+ text('./recurring:merchantAccount').should == 'SuperShopper'
414
+ end
415
+
416
+ it "includes the shopper’s reference" do
417
+ text('./recurring:shopperReference').should == 'user-id'
418
+ end
419
+
420
+ it "includes the type of contract, which is always `RECURRING'" do
421
+ text('./recurring:recurring/recurring:contract').should == 'RECURRING'
422
+ end
423
+ end
424
+
425
+ describe "list" do
426
+ before do
427
+ stub_net_http(LIST_RESPONSE)
428
+ @recurring.list
429
+ @request, @post = Net::HTTP.posted
430
+ end
431
+
432
+ after do
433
+ Net::HTTP.stubbing_enabled = false
434
+ end
435
+
436
+ it "posts the body generated for the given parameters" do
437
+ @post.body.should == @recurring.list_request_body
438
+ end
439
+
440
+ it "posts to the correct SOAP action" do
441
+ @post.soap_action.should == 'listRecurringDetails'
442
+ end
443
+
444
+ for_each_xml_backend do
445
+ it "returns a hash with parsed response details" do
446
+ @recurring.list.should == {
447
+ :creation_date => DateTime.parse('2009-10-27T11:26:22.203+01:00'),
448
+ :last_known_shopper_email => 's.hopper@example.com',
449
+ :shopper_reference => 'user-id',
450
+ :details => [
451
+ {
452
+ :card => {
453
+ :expiry_date => Date.new(2012, 12, 31),
454
+ :holder_name => 'S. Hopper',
455
+ :number => '1111'
456
+ },
457
+ :recurring_detail_reference => 'RecurringDetailReference1',
458
+ :variant => 'mc',
459
+ :creation_date => DateTime.parse('2009-10-27T11:50:12.178+01:00')
460
+ },
461
+ {
462
+ :bank => {
463
+ :bank_account_number => '123456789',
464
+ :bank_location_id => 'bank-location-id',
465
+ :bank_name => 'AnyBank',
466
+ :bic => 'BBBBCCLLbbb',
467
+ :country_code => 'NL',
468
+ :iban => 'NL69PSTB0001234567',
469
+ :owner_name => 'S. Hopper'
470
+ },
471
+ :recurring_detail_reference => 'RecurringDetailReference2',
472
+ :variant => 'IDEAL',
473
+ :creation_date => DateTime.parse('2009-10-27T11:26:22.216+01:00')
474
+ },
475
+ ],
476
+ }
477
+ end
478
+ end
479
+ end
480
+
481
+ private
482
+
483
+ def node_for_current_method
484
+ super(@recurring).xpath('//recurring:listRecurringDetails/recurring:request')
485
+ end
486
+ end
487
+ end
488
+
489
+ AUTHORISE_RESPONSE = <<EOS
490
+ <?xml version="1.0" encoding="UTF-8"?>
491
+ <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">
492
+ <soap:Body>
493
+ <ns1:authoriseResponse xmlns:ns1="http://payment.services.adyen.com">
494
+ <ns1:paymentResult>
495
+ <additionalData xmlns="http://payment.services.adyen.com" xsi:nil="true"/>
496
+ <authCode xmlns="http://payment.services.adyen.com">1234</authCode>
497
+ <dccAmount xmlns="http://payment.services.adyen.com" xsi:nil="true"/>
498
+ <dccSignature xmlns="http://payment.services.adyen.com" xsi:nil="true"/>
499
+ <fraudResult xmlns="http://payment.services.adyen.com" xsi:nil="true"/>
500
+ <issuerUrl xmlns="http://payment.services.adyen.com" xsi:nil="true"/>
501
+ <md xmlns="http://payment.services.adyen.com" xsi:nil="true"/>
502
+ <paRequest xmlns="http://payment.services.adyen.com" xsi:nil="true"/>
503
+ <pspReference xmlns="http://payment.services.adyen.com">9876543210987654</pspReference>
504
+ <refusalReason xmlns="http://payment.services.adyen.com" xsi:nil="true"/>
505
+ <resultCode xmlns="http://payment.services.adyen.com">Authorised</resultCode>
506
+ </ns1:paymentResult>
507
+ </ns1:authoriseResponse>
508
+ </soap:Body>
509
+ </soap:Envelope>
510
+ EOS
511
+
512
+ LIST_RESPONSE = <<EOS
513
+ <?xml version="1.0"?>
514
+ <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">
515
+ <soap:Body>
516
+ <ns1:listRecurringDetailsResponse xmlns:ns1="http://recurring.services.adyen.com">
517
+ <ns1:result xmlns:ns2="http://payment.services.adyen.com">
518
+ <ns1:creationDate>2009-10-27T11:26:22.203+01:00</ns1:creationDate>
519
+ <details xmlns="http://recurring.services.adyen.com">
520
+ <RecurringDetail>
521
+ <bank xsi:nil="true"/>
522
+ <card>
523
+ <cvc xmlns="http://payment.services.adyen.com" xsi:nil="true"/>
524
+ <expiryMonth xmlns="http://payment.services.adyen.com">12</expiryMonth>
525
+ <expiryYear xmlns="http://payment.services.adyen.com">2012</expiryYear>
526
+ <holderName xmlns="http://payment.services.adyen.com">S. Hopper</holderName>
527
+ <issueNumber xmlns="http://payment.services.adyen.com" xsi:nil="true"/>
528
+ <number xmlns="http://payment.services.adyen.com">1111</number>
529
+ <startMonth xmlns="http://payment.services.adyen.com" xsi:nil="true"/>
530
+ <startYear xmlns="http://payment.services.adyen.com" xsi:nil="true"/>
531
+ </card>
532
+ <creationDate>2009-10-27T11:50:12.178+01:00</creationDate>
533
+ <elv xsi:nil="true"/>
534
+ <name/>
535
+ <recurringDetailReference>RecurringDetailReference1</recurringDetailReference>
536
+ <variant>mc</variant>
537
+ </RecurringDetail>
538
+ <RecurringDetail>
539
+ <bank>
540
+ <bankAccountNumber xmlns="http://payment.services.adyen.com">123456789</bankAccountNumber>
541
+ <bankLocationId xmlns="http://payment.services.adyen.com">bank-location-id</bankLocationId>
542
+ <bankName xmlns="http://payment.services.adyen.com">AnyBank</bankName>
543
+ <bic xmlns="http://payment.services.adyen.com">BBBBCCLLbbb</bic>
544
+ <countryCode xmlns="http://payment.services.adyen.com">NL</countryCode>
545
+ <iban xmlns="http://payment.services.adyen.com">NL69PSTB0001234567</iban>
546
+ <ownerName xmlns="http://payment.services.adyen.com">S. Hopper</ownerName>
547
+ </bank>
548
+ <card xsi:nil="true"/>
549
+ <creationDate>2009-10-27T11:26:22.216+01:00</creationDate>
550
+ <elv xsi:nil="true"/>
551
+ <name/>
552
+ <recurringDetailReference>RecurringDetailReference2</recurringDetailReference>
553
+ <variant>IDEAL</variant>
554
+ </RecurringDetail>
555
+ </details>
556
+ <ns1:lastKnownShopperEmail>s.hopper@example.com</ns1:lastKnownShopperEmail>
557
+ <ns1:shopperReference>user-id</ns1:shopperReference>
558
+ </ns1:result>
559
+ </ns1:listRecurringDetailsResponse>
560
+ </soap:Body>
561
+ </soap:Envelope>
562
+ EOS