fingertips-adyen 0.3.7.20100917
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +8 -0
- data/LICENSE +20 -0
- data/README.rdoc +40 -0
- data/Rakefile +5 -0
- data/adyen.gemspec +30 -0
- data/init.rb +1 -0
- data/lib/adyen.rb +77 -0
- data/lib/adyen/api.rb +343 -0
- data/lib/adyen/encoding.rb +21 -0
- data/lib/adyen/form.rb +336 -0
- data/lib/adyen/formatter.rb +37 -0
- data/lib/adyen/matchers.rb +105 -0
- data/lib/adyen/notification.rb +151 -0
- data/spec/adyen_spec.rb +86 -0
- data/spec/api_spec.rb +562 -0
- data/spec/form_spec.rb +152 -0
- data/spec/notification_spec.rb +97 -0
- data/spec/spec_helper.rb +12 -0
- data/tasks/github-gem.rake +371 -0
- metadata +132 -0
@@ -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
|
data/spec/adyen_spec.rb
ADDED
@@ -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
|
data/spec/api_spec.rb
ADDED
@@ -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 わくわく 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
|