adyen 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,147 @@
1
+ require "handsoap"
2
+
3
+ module Adyen
4
+
5
+ # The SOAP module contains classes that interact with the Adyen
6
+ # SOAP services. The clients are based on the Handsoap library.
7
+ # Shared functionality for all services is implemented in the
8
+ # Adyen::SOAP::Base class.
9
+ #
10
+ # Note that you'll need an Adyen notification PSP reference for
11
+ # most SOAP calls. Because of this, store all notifications that
12
+ # Adyen sends to you. (e.g. using the Adyen::Notification ActiveRecord
13
+ # class). Moreover, most SOAP calls do not respond that they were
14
+ # successful immediately, but a notifications to indicate that will
15
+ # be sent later on.
16
+ #
17
+ # You'll need to provide a username and password to interact
18
+ # with the Adyen SOAP services:
19
+ #
20
+ # Adyen::SOAP.username = 'ws@Company.MyAccount'
21
+ # Adyen::SOAP.password = 'very$ecret'
22
+ #
23
+ # You can setup default values for every SOAP call that needs them:
24
+ #
25
+ # Adyen::SOAP.default_arguments[:merchent_account] = 'MyMerchant'
26
+ #
27
+ # For now, only the recurring payment service client is implemented
28
+ # (Adyen::SOAP::RecurringService).
29
+ module SOAP
30
+
31
+ class << self
32
+ # Set up accessors for HTTP Basic Authentication and
33
+ # for adding default arguments to SOAP calls.
34
+ attr_accessor :username, :password, :default_arguments
35
+ end
36
+
37
+ # Use no default arguments by default
38
+ self.default_arguments = {}
39
+
40
+ # The base class sets up XML namespaces and HTTP authentication
41
+ # for all the Adyen SOAP services
42
+ class Base < Handsoap::Service
43
+
44
+ def self.inherited(klass)
45
+ # The version must be set to construct the request envelopes,
46
+ # the URI wil be set later using the correct Adyen.environment.
47
+ klass.endpoint :version => 1, :uri => 'bogus'
48
+ end
49
+
50
+ # Setup basic auth headers in the HTTP client
51
+ def on_after_create_http_client(http_client)
52
+ debug { |logger| logger.puts "Authorization: #{Adyen::SOAP.username}:#{Adyen::SOAP.password}..." }
53
+ # http_client.userpwd = "#{Adyen::SOAP.username}:#{Adyen::SOAP.password}"
54
+ http_client.set_auth Adyen::SOAP.username, Adyen::SOAP.password
55
+ end
56
+
57
+ # Setup XML namespaces for SOAP request body
58
+ def on_create_document(doc)
59
+ doc.alias 'payment', 'http://payment.services.adyen.com'
60
+ doc.alias 'recurring', 'http://recurring.services.adyen.com'
61
+ doc.alias 'common', 'http://common.services.adyen.com'
62
+ end
63
+
64
+ # Setup XML namespaces for SOAP response
65
+ def on_response_document(doc)
66
+ doc.add_namespace 'payment', 'http://payment.services.adyen.com'
67
+ doc.add_namespace 'recurring', 'http://recurring.services.adyen.com'
68
+ doc.add_namespace 'common', 'http://common.services.adyen.com'
69
+ end
70
+
71
+ # Set endpoint URI before dispatch, so that changes in environment
72
+ # are reflected correctly.
73
+ def on_before_dispatch
74
+ self.class.endpoint(:uri => self.class::ENDPOINT_URI % Adyen.environment.to_s, :version => 1)
75
+ end
76
+ end
77
+
78
+ # SOAP client to interact with the payment modification service of Adyen.
79
+ # At this moment, none of the calls are implemented.
80
+ class PaymentService < Base
81
+
82
+ ENDPOINT_URI = 'https://pal-%s.adyen.com/pal/servlet/soap/Payment'
83
+
84
+ end
85
+
86
+ # SOAP client to interact with the recurring payment service of Adyen.
87
+ # This client implements the submitRecurring call to submit payments
88
+ # for a recurring contract. Moreover, it implements the deactiveRecurring
89
+ # call to cancel a recurring contract.
90
+ #
91
+ # See the Adyen Recurring manual for more information about this SOAP service
92
+ class RecurringService < Base
93
+
94
+ ENDPOINT_URI = 'https://pal-%s.adyen.com/pal/servlet/soap/Recurring'
95
+
96
+ # Submits a recurring payment. Requires the following arguments as hash:
97
+ #
98
+ # * <tt>:currency</tt> The currency code (EUR, GBP, USD, etc)
99
+ # * <tt>:value</tt> The value of the payments in cents
100
+ # * <tt>:merchent_account</tt> The merchant account under which to place
101
+ # this payment.
102
+ # * <tt>:recurring_reference</tt> The psp_reference of the RECURRING_CONTRACT
103
+ # notification that was sent after the initial payment.
104
+ # * <tt>:reference</tt> The (merchant) reference for this payment.
105
+ # * <tt>:shopper_email</tt> The email address of the shopper.
106
+ # * <tt>:shopper_reference</tt> The refrence of the shopper. This should be
107
+ # the same as the reference that was used to create the recurring contract.
108
+ def submit(args = {})
109
+ invoke_args = Adyen::SOAP.default_arguments.merge(args)
110
+ response = invoke('recurring:submitRecurring') do |message|
111
+ message.add('recurring:recurringRequest') do |req|
112
+ req.add('recurring:amount') do |amount|
113
+ amount.add('common:currency', invoke_args[:currency])
114
+ amount.add('common:value', invoke_args[:value])
115
+ end
116
+ req.add('recurring:merchantAccount', invoke_args[:merchant_account])
117
+ req.add('recurring:recurringReference', invoke_args[:recurring_reference])
118
+ req.add('recurring:reference', invoke_args[:reference])
119
+ req.add('recurring:shopperEmail', invoke_args[:shopper_email])
120
+ req.add('recurring:shopperReference', invoke_args[:shopper_reference])
121
+ end
122
+ end
123
+ end
124
+
125
+ # Deactivates a recurring payment contract. Requires the following arguments:
126
+ #
127
+ # * <tt>:merchent_account</tt> The merchant account under which to place
128
+ # this payment.
129
+ # * <tt>:recurring_reference</tt> The psp_reference of the RECURRING_CONTRACT
130
+ # notification that was sent after the initial payment.
131
+ # * <tt>:reference</tt> The (merchant) reference for this deactivation.
132
+ # * <tt>:shopper_reference</tt> The refrence of the shopper. This should be
133
+ # the same as the reference that was used to create the recurring contract.
134
+ def deactivate(args = {})
135
+ invoke_args = Adyen::SOAP.default_arguments.merge(args)
136
+ response = invoke('recurring:deactivateRecurring') do |message|
137
+ message.add('recurring:recurringRequest') do |req|
138
+ req.add('recurring:merchantAccount', invoke_args[:merchant_account])
139
+ req.add('recurring:recurringReference', invoke_args[:recurring_reference])
140
+ req.add('recurring:reference', invoke_args[:reference])
141
+ req.add('recurring:shopperReference', invoke_args[:shopper_reference])
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,53 @@
1
+ require "#{File.dirname(__FILE__)}/spec_helper.rb"
2
+
3
+ describe Adyen do
4
+ describe Adyen::Encoding do
5
+ it "should a hmac_base64 correcly" do
6
+ encoded_str = Adyen::Encoding.hmac_base64('bla', 'bla')
7
+ encoded_str.should_not be_blank
8
+ encoded_str.size.should == 28
9
+ end
10
+
11
+ it "should gzip_base64 correcly" do
12
+ encoded_str = Adyen::Encoding.gzip_base64('bla')
13
+ encoded_str.should_not be_blank
14
+ encoded_str.size.should == 32
15
+ end
16
+ end
17
+
18
+ describe Adyen::Formatter::DateTime do
19
+ it "should accept dates" do
20
+ Adyen::Formatter::DateTime.fmt_date(Date.today).should match(/^\d{4}-\d{2}-\d{2}$/)
21
+ end
22
+
23
+ it "should accept times" do
24
+ Adyen::Formatter::DateTime.fmt_time(Time.now).should match(/^\d{4}-\d{2}-\d{2}T\d{2}\:\d{2}\:\d{2}Z$/)
25
+ end
26
+
27
+ it "should accept valid time strings" do
28
+ 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$/)
29
+ end
30
+
31
+ it "should accept valid time strings" do
32
+ Adyen::Formatter::DateTime.fmt_date('2009-01-01').should match(/^\d{4}-\d{2}-\d{2}$/)
33
+ end
34
+
35
+ it "should raise on an invalid time string" do
36
+ lambda { Adyen::Formatter::DateTime.fmt_time('2009-01-01 11:11:11') }.should raise_error
37
+ end
38
+
39
+ it "should raise on an invalid date string" do
40
+ lambda { Adyen::Formatter::DateTime.fmt_date('2009-1-1') }.should raise_error
41
+ end
42
+ end
43
+
44
+ describe Adyen::Formatter::Price do
45
+ it "should return a Fixnum with digits only when converting to cents" do
46
+ Adyen::Formatter::Price.in_cents(33.76).should be_kind_of(Fixnum)
47
+ end
48
+
49
+ it "should return a BigDecimal when converting from cents" do
50
+ Adyen::Formatter::Price.from_cents(1234).should be_kind_of(BigDecimal)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,123 @@
1
+ require "#{File.dirname(__FILE__)}/spec_helper.rb"
2
+
3
+ describe Adyen::Form do
4
+
5
+ describe 'Action URLs' do
6
+
7
+ before(:each) do
8
+ # Use autodetection for the environment unless otherwise specified
9
+ Adyen.environment = nil
10
+ end
11
+
12
+ it "should generate correct the testing url" do
13
+ Adyen::Form.url.should eql('https://test.adyen.com/hpp/select.shtml')
14
+ end
15
+
16
+ it "should generate a live url if the environemtn is set top live" do
17
+ Adyen.environment = :live
18
+ Adyen::Form.url.should eql('https://live.adyen.com/hpp/select.shtml')
19
+ end
20
+
21
+ it "should generate correct live url in a production environment" do
22
+ Adyen.stub!(:autodetect_environment).and_return('live')
23
+ Adyen::Form.url.should eql('https://live.adyen.com/hpp/select.shtml')
24
+ end
25
+
26
+ it "should generate correct live url if explicitely asked for" do
27
+ Adyen::Form.url(:live).should eql('https://live.adyen.com/hpp/select.shtml')
28
+ end
29
+ end
30
+
31
+ describe 'redirect signature check' do
32
+ before(:each) do
33
+ # Example taken from integration manual
34
+
35
+ # Shared secret between you and Adyen, only valid for this skinCode!
36
+ @shared_secret = 'Kah942*$7sdp0)'
37
+
38
+ # Example get params sent back with redirect
39
+ @params = { :authResult => 'AUTHORISED', :pspReference => '1211992213193029',
40
+ :merchantReference => 'Internet Order 12345', :skinCode => '4aD37dJA',
41
+ :merchantSig => 'ytt3QxWoEhAskUzUne0P5VA9lPw='}
42
+ end
43
+
44
+ it "should calculate the signature string correctly" do
45
+ Adyen::Form.redirect_signature_string(@params).should eql('AUTHORISED1211992213193029Internet Order 123454aD37dJA')
46
+ end
47
+
48
+ it "should calculate the signature correctly" do
49
+ Adyen::Form.redirect_signature(@params, @shared_secret).should eql(@params[:merchantSig])
50
+ end
51
+
52
+ it "should check the signature correctly" do
53
+ Adyen::Form.redirect_signature_check(@params, @shared_secret).should be_true
54
+ end
55
+
56
+ it "should detect a tampered field" do
57
+ Adyen::Form.redirect_signature_check(@params.merge(:pspReference => 'tampered'), @shared_secret).should be_false
58
+ end
59
+
60
+ it "should detect a tampered signature" do
61
+ Adyen::Form.redirect_signature_check(@params.merge(:merchantSig => 'tampered'), @shared_secret).should be_false
62
+ end
63
+
64
+ end
65
+
66
+ describe 'hidden fields generation' do
67
+
68
+ include ActionView::Helpers::TagHelper
69
+
70
+ before(:each) do
71
+ @attributes = { :currency_code => 'GBP', :payment_amount => 10000, :ship_before_date => Date.today,
72
+ :merchant_reference => 'Internet Order 12345', :skin_code => '4aD37dJA',
73
+ :merchant_account => 'TestMerchant', :session_validity => 1.hour.from_now }
74
+ end
75
+
76
+ it "should generate a valid payment form" do
77
+ content_tag(:form, Adyen::Form.hidden_fields(@attributes.merge(:shared_secret => 'secret')),
78
+ :action => Adyen::Form.url, :method => :post).should have_adyen_payment_form
79
+ end
80
+ end
81
+
82
+ describe 'signature calculation' do
83
+
84
+ # This example is taken from the Adyen integration manual
85
+
86
+ before(:each) do
87
+ @attributes = { :currency_code => 'GBP', :payment_amount => 10000,
88
+ :ship_before_date => '2007-10-20', :merchant_reference => 'Internet Order 12345',
89
+ :skin_code => '4aD37dJA', :merchant_account => 'TestMerchant',
90
+ :session_validity => '2007-10-11T11:00:00Z' }
91
+
92
+ Adyen::Form.do_attribute_transformations!(@attributes)
93
+ end
94
+
95
+ it "should construct the signature string correctly" do
96
+ signature_string = Adyen::Form.calculate_signature_string(@attributes)
97
+ signature_string.should eql("10000GBP2007-10-20Internet Order 123454aD37dJATestMerchant2007-10-11T11:00:00Z")
98
+ end
99
+
100
+ it "should calculate the signature correctly" do
101
+ signature = Adyen::Form.calculate_signature(@attributes.merge(:shared_secret => 'Kah942*$7sdp0)'))
102
+ signature.should eql('x58ZcRVL1H6y+XSeBGrySJ9ACVo=')
103
+ end
104
+
105
+ it "should calculate the signature correctly for a recurring payment" do
106
+ # Add the required recurrent payment attributes
107
+ @attributes.merge!(:recurring_contract => 'DEFAULT', :shopper_reference => 'grasshopper52', :shopper_email => 'gras.shopper@somewhere.org')
108
+
109
+ signature_string = Adyen::Form.calculate_signature_string(@attributes)
110
+ signature_string.should eql("10000GBP2007-10-20Internet Order 123454aD37dJATestMerchant2007-10-11T11:00:00Zgras.shopper@somewhere.orggrasshopper52DEFAULT")
111
+ end
112
+
113
+ it "should calculate the signature correctly for a recurring payment" do
114
+ # Add the required recurrent payment attributes
115
+ @attributes.merge!(:recurring_contract => 'DEFAULT', :shopper_reference => 'grasshopper52', :shopper_email => 'gras.shopper@somewhere.org')
116
+
117
+ signature = Adyen::Form.calculate_signature(@attributes.merge(:shared_secret => 'Kah942*$7sdp0)'))
118
+ signature.should eql('F2BQEYbE+EUhiRGuPtcD16Gm7JY=')
119
+ end
120
+
121
+ end
122
+
123
+ end
@@ -0,0 +1,97 @@
1
+ require "#{File.dirname(__FILE__)}/spec_helper.rb"
2
+
3
+ describe Adyen::Notification do
4
+
5
+ before(:all) do
6
+ ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:')
7
+
8
+ ActiveRecord::Migration.verbose = false
9
+ Adyen::Notification::Migration.up
10
+ end
11
+
12
+ after(:all) do
13
+ Adyen::Notification::Migration.down
14
+ end
15
+
16
+ describe Adyen::Notification::HttpPost do
17
+
18
+ describe 'receiving payment authorization notification' do
19
+
20
+ before(:each) do
21
+ @request = mock('request')
22
+ @request.stub!(:params).and_return({
23
+ "merchantAccountCode"=>"FloorPlannerNL", "eventCode"=>"AUTHORISATION",
24
+ "paymentMethod"=>"mc", "eventDate"=>"2009-08-10T09:00:08.04Z",
25
+ "operations"=>"CANCEL,CAPTURE,REFUND", "merchantReference"=>"4",
26
+ "action"=>"process_adyen", "live"=>"false", "controller"=>"payment_notifications",
27
+ "value"=>"2500", "success"=>"false", "reason"=>"10676:1111:12/2012",
28
+ "originalReference"=>"", "pspReference"=>"8712498948081194", "currency"=>"USD"})
29
+
30
+ @notification = Adyen::Notification::HttpPost.log(@request)
31
+ end
32
+
33
+ after(:each) { @notification.destroy }
34
+
35
+ it "should have saved the notification record" do
36
+ @notification.should_not be_new_record
37
+ end
38
+
39
+ it "should be an authorization" do
40
+ @notification.should be_authorisation
41
+ end
42
+
43
+ it "should convert the amount to a bigdecimal" do
44
+ @notification.value.should eql(BigDecimal.new('25.00'))
45
+ end
46
+
47
+ it "should convert live to a boolean" do
48
+ @notification.should_not be_live
49
+ end
50
+
51
+ it "should convert success to a boolean" do
52
+ @notification.should_not be_success
53
+ end
54
+
55
+ it "should not be a successfull authorization" do
56
+ @notification.should_not be_successful_authorization
57
+ end
58
+
59
+ it "should convert the eventDate" do
60
+ @notification.event_date.should be_kind_of(Time)
61
+ end
62
+
63
+ it "should convert the empty original reference to NULL" do
64
+ @notification.original_reference.should be_nil
65
+ end
66
+ end
67
+
68
+ context 'duplicate detection' do
69
+ before(:each) do
70
+
71
+ @fields = { "merchantAccountCode"=>"FloorPlannerNL", "eventCode"=>"AUTHORISATION",
72
+ "paymentMethod"=>"mc", "eventDate"=>"2009-08-10T09:00:08.04Z",
73
+ "operations"=>"CANCEL,CAPTURE,REFUND", "merchantReference"=>"4",
74
+ "action"=>"process_adyen", "live"=>"false", "controller"=>"payment_notifications",
75
+ "value"=>"2500", "success"=>"false", "reason"=>"10676:1111:12/2012",
76
+ "originalReference"=>"", "pspReference"=>"8712498948081194", "currency"=>"USD"}
77
+
78
+ @request = mock('request')
79
+ @request.stub!(:params).and_return(@fields)
80
+ @notification = Adyen::Notification::HttpPost.log(@request)
81
+ end
82
+
83
+ after(:each) { @notification.destroy }
84
+
85
+ it "should raise an error on a duplicate notification" do
86
+ lambda { Adyen::Notification::HttpPost.log(@request) }.should raise_error(ActiveRecord::RecordInvalid)
87
+ end
88
+
89
+ it "should not raise an error on a when success is set to true" do
90
+ second_request = mock('request')
91
+ second_request.stub!(:params).and_return(@fields.merge('success' => 'true'))
92
+ lambda { Adyen::Notification::HttpPost.log(second_request) }.should_not raise_error
93
+ end
94
+
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,30 @@
1
+ require "#{File.dirname(__FILE__)}/spec_helper.rb"
2
+
3
+ describe Adyen::SOAP::RecurringService do
4
+
5
+ before(:each) do
6
+
7
+ response_xml = <<-EOS
8
+ <?xml version="1.0" encoding="UTF-8" ?>
9
+ <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
10
+ xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" xmlns:xsd="http://www.w3.org/1999/XMLSchema">
11
+
12
+ <soap:Body>
13
+ </soap:Body>
14
+ </soap:Envelope>
15
+ EOS
16
+
17
+ @response = mock('Handsoap::HTTP::Response')
18
+ @part = mock('Handsoap::HTTP::Part')
19
+ @part.stub!(:body).and_return(response_xml)
20
+
21
+ @response.stub!(:status).and_return(200)
22
+ @response.stub!(:primary_part).and_return(@part)
23
+ Adyen::SOAP::RecurringService.instance.stub!(:send_http_request).and_return(@response)
24
+ end
25
+
26
+ it "should send an HTTP request" do
27
+ Adyen::SOAP::RecurringService.instance.should_receive(:send_http_request).and_return(@response)
28
+ Adyen::SOAP::RecurringService.submit()
29
+ end
30
+ end