floorplanner-adyen 0.2.2

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,153 @@
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
+ # Handsoap BUG: Setting headers does not work, using a Curb specific method for now.
54
+ # auth = Base64.encode64("#{Adyen::SOAP.username}:#{Adyen::SOAP.password}").chomp
55
+ # http_client.headers['Authorization'] = "Basic #{auth}"
56
+ http_client.userpwd = "#{Adyen::SOAP.username}:#{Adyen::SOAP.password}"
57
+ # We explicitly set the http_client, curb in this case, to follow the location.
58
+ # By doing so, we avoid the cryptic Curl::Err::TooManyRedirectsError error.
59
+ http_client.follow_location = true
60
+ http_client.max_redirects = 1
61
+ end
62
+
63
+ # Setup XML namespaces for SOAP request body
64
+ def on_create_document(doc)
65
+ doc.alias 'payment', 'http://payment.services.adyen.com'
66
+ doc.alias 'recurring', 'http://recurring.services.adyen.com'
67
+ doc.alias 'common', 'http://common.services.adyen.com'
68
+ end
69
+
70
+ # Setup XML namespaces for SOAP response
71
+ def on_response_document(doc)
72
+ doc.add_namespace 'payment', 'http://payment.services.adyen.com'
73
+ doc.add_namespace 'recurring', 'http://recurring.services.adyen.com'
74
+ doc.add_namespace 'common', 'http://common.services.adyen.com'
75
+ end
76
+
77
+ # Set endpoint URI before dispatch, so that changes in environment
78
+ # are reflected correctly.
79
+ def on_before_dispatch
80
+ self.class.endpoint(:uri => self.class::ENDPOINT_URI % Adyen.environment.to_s, :version => 1)
81
+ end
82
+ end
83
+
84
+ # SOAP client to interact with the payment modification service of Adyen.
85
+ # At this moment, none of the calls are implemented.
86
+ class PaymentService < Base
87
+
88
+ ENDPOINT_URI = 'https://pal-%s.adyen.com/pal/servlet/soap/Payment'
89
+
90
+ end
91
+
92
+ # SOAP client to interact with the recurring payment service of Adyen.
93
+ # This client implements the submitRecurring call to submit payments
94
+ # for a recurring contract. Moreover, it implements the deactiveRecurring
95
+ # call to cancel a recurring contract.
96
+ #
97
+ # See the Adyen Recurring manual for more information about this SOAP service
98
+ class RecurringService < Base
99
+
100
+ ENDPOINT_URI = 'https://pal-%s.adyen.com/pal/servlet/soap/Recurring'
101
+
102
+ # Submits a recurring payment. Requires the following arguments as hash:
103
+ #
104
+ # * <tt>:currency</tt> The currency code (EUR, GBP, USD, etc)
105
+ # * <tt>:value</tt> The value of the payments in cents
106
+ # * <tt>:merchent_account</tt> The merchant account under which to place
107
+ # this payment.
108
+ # * <tt>:recurring_reference</tt> The psp_reference of the RECURRING_CONTRACT
109
+ # notification that was sent after the initial payment.
110
+ # * <tt>:reference</tt> The (merchant) reference for this payment.
111
+ # * <tt>:shopper_email</tt> The email address of the shopper.
112
+ # * <tt>:shopper_reference</tt> The refrence of the shopper. This should be
113
+ # the same as the reference that was used to create the recurring contract.
114
+ def submit(args = {})
115
+ invoke_args = Adyen::SOAP.default_arguments.merge(args)
116
+ response = invoke('recurring:submitRecurring') do |message|
117
+ message.add('recurring:recurringRequest') do |req|
118
+ req.add('recurring:amount') do |amount|
119
+ amount.add('common:currency', invoke_args[:currency])
120
+ amount.add('common:value', invoke_args[:value])
121
+ end
122
+ req.add('recurring:merchantAccount', invoke_args[:merchant_account])
123
+ req.add('recurring:recurringReference', invoke_args[:recurring_reference])
124
+ req.add('recurring:reference', invoke_args[:reference])
125
+ req.add('recurring:shopperEmail', invoke_args[:shopper_email])
126
+ req.add('recurring:shopperReference', invoke_args[:shopper_reference])
127
+ end
128
+ end
129
+ end
130
+
131
+ # Deactivates a recurring payment contract. Requires the following arguments:
132
+ #
133
+ # * <tt>:merchent_account</tt> The merchant account under which to place
134
+ # this payment.
135
+ # * <tt>:recurring_reference</tt> The psp_reference of the RECURRING_CONTRACT
136
+ # notification that was sent after the initial payment.
137
+ # * <tt>:reference</tt> The (merchant) reference for this deactivation.
138
+ # * <tt>:shopper_reference</tt> The refrence of the shopper. This should be
139
+ # the same as the reference that was used to create the recurring contract.
140
+ def deactivate(args = {})
141
+ invoke_args = Adyen::SOAP.default_arguments.merge(args)
142
+ response = invoke('recurring:deactivateRecurring') do |message|
143
+ message.add('recurring:recurringRequest') do |req|
144
+ req.add('recurring:merchantAccount', invoke_args[:merchant_account])
145
+ req.add('recurring:recurringReference', invoke_args[:recurring_reference])
146
+ req.add('recurring:reference', invoke_args[:reference])
147
+ req.add('recurring:shopperReference', invoke_args[:shopper_reference])
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
153
+ 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,152 @@
1
+ require "#{File.dirname(__FILE__)}/spec_helper.rb"
2
+
3
+ describe Adyen::Form do
4
+
5
+ before(:all) do
6
+ Adyen::Form.register_skin(:testing, '4aD37dJA', 'Kah942*$7sdp0)')
7
+ Adyen::Form.default_parameters[:merchant_account] = 'TestMerchant'
8
+ end
9
+
10
+ describe 'Action URLs' do
11
+
12
+ before(:each) do
13
+ # Use autodetection for the environment unless otherwise specified
14
+ Adyen.environment = nil
15
+ end
16
+
17
+ it "should generate correct the testing url" do
18
+ Adyen::Form.url.should eql('https://test.adyen.com/hpp/select.shtml')
19
+ end
20
+
21
+ it "should generate a live url if the environemtn is set top live" do
22
+ Adyen.environment = :live
23
+ Adyen::Form.url.should eql('https://live.adyen.com/hpp/select.shtml')
24
+ end
25
+
26
+ it "should generate correct live url in a production environment" do
27
+ Adyen.stub!(:autodetect_environment).and_return('live')
28
+ Adyen::Form.url.should eql('https://live.adyen.com/hpp/select.shtml')
29
+ end
30
+
31
+ it "should generate correct live url if explicitely asked for" do
32
+ Adyen::Form.url(:live).should eql('https://live.adyen.com/hpp/select.shtml')
33
+ end
34
+ end
35
+
36
+ describe 'redirect signature check' do
37
+ before(:each) do
38
+ # Example taken from integration manual
39
+
40
+ # Example get params sent back with redirect
41
+ @params = { :authResult => 'AUTHORISED', :pspReference => '1211992213193029',
42
+ :merchantReference => 'Internet Order 12345', :skinCode => '4aD37dJA',
43
+ :merchantSig => 'ytt3QxWoEhAskUzUne0P5VA9lPw='}
44
+ end
45
+
46
+ it "should calculate the signature string correctly" do
47
+ Adyen::Form.redirect_signature_string(@params).should eql('AUTHORISED1211992213193029Internet Order 123454aD37dJA')
48
+ end
49
+
50
+ it "should calculate the signature correctly" do
51
+ Adyen::Form.redirect_signature(@params).should eql(@params[:merchantSig])
52
+ end
53
+
54
+ it "should check the signature correctly with explicit shared signature" do
55
+ Adyen::Form.redirect_signature_check(@params, 'Kah942*$7sdp0)').should be_true
56
+ end
57
+
58
+ it "should check the signature correctly using the stored shared secret" do
59
+ Adyen::Form.redirect_signature_check(@params).should be_true
60
+ end
61
+
62
+
63
+ it "should detect a tampered field" do
64
+ Adyen::Form.redirect_signature_check(@params.merge(:pspReference => 'tampered')).should be_false
65
+ end
66
+
67
+ it "should detect a tampered signature" do
68
+ Adyen::Form.redirect_signature_check(@params.merge(:merchantSig => 'tampered')).should be_false
69
+ end
70
+
71
+ end
72
+
73
+ describe 'redirect URL generation' do
74
+ before(:each) do
75
+ @attributes = { :currency_code => 'GBP', :payment_amount => 10000, :ship_before_date => Date.today,
76
+ :merchant_reference => 'Internet Order 12345', :skin => :testing,
77
+ :session_validity => 1.hour.from_now }
78
+
79
+ @redirect_url = Adyen::Form.redirect_url(@attributes)
80
+ end
81
+
82
+ it "should return an URL pointing to the adyen server" do
83
+ @redirect_url.should =~ %r[^#{Adyen::Form.url}]
84
+ end
85
+
86
+ it "should include all provided attributes" do
87
+ params = @redirect_url.split('?', 2).last.split('&').map { |param| param.split('=', 2).first }
88
+ params.should include(*(@attributes.keys.map { |k| k.to_s.camelize(:lower) }))
89
+ end
90
+
91
+ it "should include the merchant signature" do
92
+ params = @redirect_url.split('?', 2).last.split('&').map { |param| param.split('=', 2).first }
93
+ params.should include('merchantSig')
94
+ end
95
+ end
96
+
97
+ describe 'hidden fields generation' do
98
+
99
+ include ActionView::Helpers::TagHelper
100
+
101
+ before(:each) do
102
+ @attributes = { :currency_code => 'GBP', :payment_amount => 10000, :ship_before_date => Date.today,
103
+ :merchant_reference => 'Internet Order 12345', :skin => :testing,
104
+ :session_validity => 1.hour.from_now }
105
+ end
106
+
107
+ it "should generate a valid payment form" do
108
+ content_tag(:form, Adyen::Form.hidden_fields(@attributes),
109
+ :action => Adyen::Form.url, :method => :post).should have_adyen_payment_form
110
+ end
111
+ end
112
+
113
+ describe 'signature calculation' do
114
+
115
+ # This example is taken from the Adyen integration manual
116
+
117
+ before(:each) do
118
+
119
+ @parameters = { :currency_code => 'GBP', :payment_amount => 10000,
120
+ :ship_before_date => '2007-10-20', :merchant_reference => 'Internet Order 12345',
121
+ :skin => :testing, :session_validity => '2007-10-11T11:00:00Z' }
122
+
123
+ Adyen::Form.do_parameter_transformations!(@parameters)
124
+ end
125
+
126
+ it "should construct the signature string correctly" do
127
+ signature_string = Adyen::Form.calculate_signature_string(@parameters)
128
+ signature_string.should eql("10000GBP2007-10-20Internet Order 123454aD37dJATestMerchant2007-10-11T11:00:00Z")
129
+ end
130
+
131
+ it "should calculate the signature correctly" do
132
+ signature = Adyen::Form.calculate_signature(@parameters)
133
+ signature.should eql('x58ZcRVL1H6y+XSeBGrySJ9ACVo=')
134
+ end
135
+
136
+ it "should calculate the signature correctly for a recurring payment" do
137
+ # Add the required recurrent payment attributes
138
+ @parameters.merge!(:recurring_contract => 'DEFAULT', :shopper_reference => 'grasshopper52', :shopper_email => 'gras.shopper@somewhere.org')
139
+
140
+ signature_string = Adyen::Form.calculate_signature_string(@parameters)
141
+ signature_string.should eql("10000GBP2007-10-20Internet Order 123454aD37dJATestMerchant2007-10-11T11:00:00Zgras.shopper@somewhere.orggrasshopper52DEFAULT")
142
+ end
143
+
144
+ it "should calculate the signature correctly for a recurring payment" do
145
+ # Add the required recurrent payment attributes
146
+ @parameters.merge!(:recurring_contract => 'DEFAULT', :shopper_reference => 'grasshopper52', :shopper_email => 'gras.shopper@somewhere.org')
147
+
148
+ signature = Adyen::Form.calculate_signature(@parameters)
149
+ signature.should eql('F2BQEYbE+EUhiRGuPtcD16Gm7JY=')
150
+ end
151
+ end
152
+ 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