adyen 0.1.4

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,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