fingertips-adyen 0.3.7.20100917

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.
@@ -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