wvanbergen-adyen 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +15 -83
- data/lib/adyen/encoding.rb +21 -0
- data/lib/adyen/form.rb +13 -0
- data/lib/adyen/formatter.rb +37 -0
- data/lib/adyen/notification.rb +101 -0
- data/lib/adyen/soap.rb +149 -0
- data/spec/adyen_spec.rb +53 -0
- data/spec/form_spec.rb +35 -0
- data/spec/notification_spec.rb +101 -0
- data/spec/soap_spec.rb +5 -0
- data/tasks/github-gem.rake +38 -21
- metadata +12 -2
data/README.rdoc
CHANGED
@@ -1,100 +1,32 @@
|
|
1
1
|
= Adyen
|
2
2
|
|
3
3
|
Package to simplify including the Adyen payments services into a Ruby on Rails application.
|
4
|
-
Currently, the package contains functions to easily generate the required hidden fields and
|
5
|
-
matchers to easily check your views using rspec.
|
6
4
|
|
7
|
-
|
8
|
-
* For more information about integrating Adyen, see their manuals at
|
9
|
-
http://support.adyen.com/links/documentation
|
10
|
-
|
11
|
-
== Skins
|
12
|
-
|
13
|
-
Adyen using the notion of "skins" to determine what payment methods should be available
|
14
|
-
and what the payment environment should look like. At least one skin is required, which
|
15
|
-
can be created in the merchant area of Adyen. For every key, a shared secret is generated
|
16
|
-
that is required to sign payment forms. You will need to provide this secret as the
|
17
|
-
:shared_secret field to the hidden_fields method (see below).
|
18
|
-
|
19
|
-
== Building payment forms
|
20
|
-
|
21
|
-
<% form_tag(:url => Adyen::Form.url) do %>
|
22
|
-
<%= Adyen::Form.hidden_fields(:merchant_account => 'myaccount', ... ,
|
23
|
-
:skin_code => 'myperfectskin', :shared_secret => 'youllneverguess')
|
24
|
-
...
|
25
|
-
<%= submit_tag('pay') %>
|
26
|
-
<% end %>
|
27
|
-
|
28
|
-
Please refer to the Adyen integration manual for all the
|
29
|
-
|
30
|
-
<tt>Adyen::Form.url</tt> will return the URL to the live environment of Adyen in production
|
31
|
-
mode, otherwise it will return the testing environment. To override this behavior, use:
|
32
|
-
|
33
|
-
<% form_tag(:url => Adyen::Form.url('live')) do %>
|
34
|
-
...
|
35
|
-
<% end %>
|
36
|
-
|
37
|
-
<tt>Adyen::Form.hidden_fields</tt> will generate the hidden fields for the key/value pairs
|
38
|
-
you provide to the function. The keys will be camelized automatically. Some notes:
|
5
|
+
Adyen integration relies on three modes of communication between Adyen, your server and your client/customer:
|
39
6
|
|
40
|
-
*
|
41
|
-
*
|
42
|
-
*
|
43
|
-
* <tt>:merchant_sig</tt> will be computed automatically using this secret.
|
7
|
+
* Client-to-Adyen communication using forms and redirects.
|
8
|
+
* Adyen-to-server communications using notifications.
|
9
|
+
* Server-to-Adyen communication using SOAP services.
|
44
10
|
|
45
|
-
|
11
|
+
This library aims to ease the implementation of all these modes into your application. Moreover, it provides matchers, assertions and mocks to make it easier to implement an automated test suite to assert the integration is working correctly.
|
46
12
|
|
47
|
-
|
13
|
+
== Installation
|
48
14
|
|
49
|
-
|
50
|
-
...
|
51
|
-
config.include Adyen::Matchers, :type => :views
|
52
|
-
...
|
53
|
-
end
|
15
|
+
This plugin can either be installed as gem or Rails plugin:
|
54
16
|
|
55
|
-
|
56
|
-
|
57
|
-
By passing a hash, you can check the values of the hidden fields. By passing :anything
|
58
|
-
as value, the matcher will simply check if the hidden field exists and ignore its value.
|
17
|
+
gem install wvanbergen-adyen --source http://gems.github.com # as gem
|
18
|
+
script/plugin install git://github.com/wvanbergen/adyen.git # as plugin
|
59
19
|
|
60
|
-
|
20
|
+
== Usage
|
61
21
|
|
62
|
-
|
63
|
-
render 'payments/new.html.erb'
|
64
|
-
end
|
22
|
+
See the project wiki on http://wiki.github.com/wvanbergen/adyen to get started.
|
65
23
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
end
|
70
|
-
|
71
|
-
it "should contain an Adyen recurrent payment form" do
|
72
|
-
response.should have_adyen_recurrent_payment_form
|
73
|
-
end
|
74
|
-
|
75
|
-
it "should contain an Adyen recurrent payment form" do
|
76
|
-
response.should have_adyen_single_payment_form(:merchant_reference => :anything)
|
77
|
-
end
|
78
|
-
|
79
|
-
== Testing payment forms using assertions
|
80
|
-
|
81
|
-
To use the assertions in unit tests, first include the matchers module in your test class:
|
82
|
-
|
83
|
-
class PaymentControllerTest < Test::Unit
|
84
|
-
include Adyen::Matchers
|
85
|
-
...
|
86
|
-
|
87
|
-
Use the assertion methods <tt>assert_adyen_payment_form</tt>, <tt>assert_adyen_single_payment_form</tt>
|
88
|
-
and <tt>assert_adyen_recurring_payment_form</tt>. They work similarly to the RSpec matcher methods
|
89
|
-
described above. An example:
|
90
|
-
|
91
|
-
def test_payment_form
|
92
|
-
get new_payment_path
|
93
|
-
assert_adyen_payment_form(@response, :currency_code => 'EUR', :payment_amount => 1000)
|
94
|
-
end
|
24
|
+
* For more information about Adyen, see http://www.adyen.com
|
25
|
+
* For more information about integrating Adyen, see their manuals at
|
26
|
+
http://support.adyen.com/links/documentation
|
95
27
|
|
96
28
|
== About
|
97
29
|
|
98
30
|
This package is written by Michel Barbosa and Willem van Bergen for Floorplanner.com,
|
99
31
|
and made public under the MIT license (see LICENSE). It comes without warranty of any kind,
|
100
|
-
so use at your own risk.
|
32
|
+
so use at your own risk.
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'openssl'
|
3
|
+
require 'stringio'
|
4
|
+
require 'zlib'
|
5
|
+
|
6
|
+
module Adyen
|
7
|
+
module Encoding
|
8
|
+
def self.hmac_base64(hmac_key, message)
|
9
|
+
digest = OpenSSL::HMAC.digest(OpenSSL::Digest::Digest.new('sha1'), hmac_key, message)
|
10
|
+
Base64.encode64(digest).strip
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.gzip_base64(message)
|
14
|
+
sio = StringIO.new
|
15
|
+
gz = Zlib::GzipWriter.new(sio)
|
16
|
+
gz.write(message)
|
17
|
+
gz.close
|
18
|
+
Base64.encode64(sio.string).gsub("\n", "")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/adyen/form.rb
CHANGED
@@ -36,6 +36,7 @@ module Adyen
|
|
36
36
|
attributes[:session_validity] = Adyen::Formatter::DateTime.fmt_time(attributes[:session_validity])
|
37
37
|
end
|
38
38
|
|
39
|
+
|
39
40
|
def self.hidden_fields(attributes = {})
|
40
41
|
do_attribute_transformations!(attributes)
|
41
42
|
|
@@ -54,5 +55,17 @@ module Adyen
|
|
54
55
|
}.join("\n")
|
55
56
|
end
|
56
57
|
|
58
|
+
def self.redirect_signature_string(params)
|
59
|
+
params[:authResult].to_s + params[:pspReference].to_s + params[:merchantReference].to_s + params[:skinCode].to_s
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.redirect_signature(params, shared_secret)
|
63
|
+
Adyen::Encoding.hmac_base64(shared_secret, redirect_signature_string(params))
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.redirect_signature_check(params, shared_secret)
|
67
|
+
params[:merchantSig] == redirect_signature(params, shared_secret)
|
68
|
+
end
|
69
|
+
|
57
70
|
end
|
58
71
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Adyen
|
2
|
+
module Formatter
|
3
|
+
module DateTime
|
4
|
+
# Returns a valid Adyen string representation for a date
|
5
|
+
def self.fmt_date(date)
|
6
|
+
case date
|
7
|
+
when Date, DateTime, Time
|
8
|
+
date.strftime('%Y-%m-%d')
|
9
|
+
else
|
10
|
+
raise "Invalid date notation: #{date.inspect}!" unless /^\d{4}-\d{2}-\d{2}$/ =~ date
|
11
|
+
date
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns a valid Adyen string representation for a timestamp
|
16
|
+
def self.fmt_time(time)
|
17
|
+
case time
|
18
|
+
when Date, DateTime, Time
|
19
|
+
time.strftime('%Y-%m-%dT%H:%M:%SZ')
|
20
|
+
else
|
21
|
+
raise "Invalid timestamp notation: #{time.inspect}!" unless /^\d{4}-\d{2}-\d{2}T\d{2}\:\d{2}\:\d{2}Z$/ =~ time
|
22
|
+
time
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
module Price
|
28
|
+
def self.in_cents(price)
|
29
|
+
((price * 100).round).to_i
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.from_cents(price)
|
33
|
+
BigDecimal.new(price.to_s) / 100
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'activerecord'
|
2
|
+
|
3
|
+
module Adyen
|
4
|
+
class Notification < ActiveRecord::Base
|
5
|
+
|
6
|
+
DEFAULT_TABLE_NAME = :adyen_notifications
|
7
|
+
set_table_name(DEFAULT_TABLE_NAME)
|
8
|
+
|
9
|
+
validates_presence_of :event_code
|
10
|
+
validates_presence_of :psp_reference
|
11
|
+
validates_uniqueness_of :success, :scope => [:psp_reference, :event_code]
|
12
|
+
|
13
|
+
# Make sure we don't end up with an original_reference with an empty string
|
14
|
+
before_validation { |notification| notification.original_reference = nil if notification.original_reference.blank? }
|
15
|
+
|
16
|
+
def self.log(params)
|
17
|
+
converted_params = {}
|
18
|
+
# Convert each attribute from CamelCase notation to under_score notation
|
19
|
+
# For example, merchantReference will be converted to merchant_reference
|
20
|
+
params.each do |key, value|
|
21
|
+
field_name = key.to_s.underscore
|
22
|
+
converted_params[field_name] = value if self.column_names.include?(field_name)
|
23
|
+
end
|
24
|
+
self.create!(converted_params)
|
25
|
+
end
|
26
|
+
|
27
|
+
def authorisation?
|
28
|
+
event_code == 'AUTHORISATION'
|
29
|
+
end
|
30
|
+
|
31
|
+
alias :authorization? :authorisation?
|
32
|
+
|
33
|
+
def successful_authorisation?
|
34
|
+
event_code == 'AUTHORISATION' && success?
|
35
|
+
end
|
36
|
+
|
37
|
+
def collect_payment_for_recurring_contract!(options)
|
38
|
+
# Make sure we convert the value to cents
|
39
|
+
options[:value] = Adyen::Formatter::Price.in_cents(options[:value])
|
40
|
+
raise "This is not a recurring contract!" unless event_code == 'RECURRING_CONTRACT'
|
41
|
+
Adyen::SOAP::RecurringService.submit(options.merge(:recurring_reference => self.psp_reference))
|
42
|
+
end
|
43
|
+
|
44
|
+
def deactivate_recurring_contract!(options)
|
45
|
+
raise "This is not a recurring contract!" unless event_code == 'RECURRING_CONTRACT'
|
46
|
+
Adyen::SOAP::RecurringService.deactivate(options.merge(:recurring_reference => self.psp_reference))
|
47
|
+
end
|
48
|
+
|
49
|
+
alias :successful_authorization? :successful_authorisation?
|
50
|
+
|
51
|
+
class HttpPost < Notification
|
52
|
+
|
53
|
+
def self.log(request)
|
54
|
+
super(request.params)
|
55
|
+
end
|
56
|
+
|
57
|
+
def live=(value)
|
58
|
+
self.write_attribute(:live, [true, 1, '1', 'true'].include?(value))
|
59
|
+
end
|
60
|
+
|
61
|
+
def success=(value)
|
62
|
+
self.write_attribute(:success, [true, 1, '1', 'true'].include?(value))
|
63
|
+
end
|
64
|
+
|
65
|
+
def value=(value)
|
66
|
+
self.write_attribute(:value, Adyen::Formatter::Price.from_cents(value)) unless value.blank?
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class Migration < ActiveRecord::Migration
|
71
|
+
|
72
|
+
def self.up(table_name = Adyen::Notification::DEFAULT_TABLE_NAME)
|
73
|
+
create_table(table_name) do |t|
|
74
|
+
t.boolean :live, :null => false, :default => false
|
75
|
+
t.string :event_code, :null => false
|
76
|
+
t.string :psp_reference, :null => false
|
77
|
+
t.string :original_reference, :null => true
|
78
|
+
t.string :merchant_reference, :null => false
|
79
|
+
t.string :merchant_account_code, :null => false
|
80
|
+
t.datetime :event_date, :null => false
|
81
|
+
t.boolean :success, :null => false, :default => false
|
82
|
+
t.string :payment_method, :null => true
|
83
|
+
t.string :operations, :null => true
|
84
|
+
t.text :reason
|
85
|
+
t.string :currency, :null => false, :limit => 3
|
86
|
+
t.decimal :value, :null => true, :precision => 9, :scale => 2
|
87
|
+
t.boolean :processed, :null => false, :default => false
|
88
|
+
t.timestamps
|
89
|
+
end
|
90
|
+
add_index table_name, [:psp_reference, :event_code, :success], :unique => true
|
91
|
+
end
|
92
|
+
|
93
|
+
def self.down(table_name = Adyen::Notification::DEFAULT_TABLE_NAME)
|
94
|
+
remove_index(table_name, [:psp_reference, :event_code, :success])
|
95
|
+
drop_table(table_name)
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
end
|
data/lib/adyen/soap.rb
ADDED
@@ -0,0 +1,149 @@
|
|
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
|
+
end
|
58
|
+
|
59
|
+
# Setup XML namespaces for SOAP request body
|
60
|
+
def on_create_document(doc)
|
61
|
+
doc.alias 'payment', 'http://payment.services.adyen.com'
|
62
|
+
doc.alias 'recurring', 'http://recurring.services.adyen.com'
|
63
|
+
doc.alias 'common', 'http://common.services.adyen.com'
|
64
|
+
end
|
65
|
+
|
66
|
+
# Setup XML namespaces for SOAP response
|
67
|
+
def on_response_document(doc)
|
68
|
+
doc.add_namespace 'payment', 'http://payment.services.adyen.com'
|
69
|
+
doc.add_namespace 'recurring', 'http://recurring.services.adyen.com'
|
70
|
+
doc.add_namespace 'common', 'http://common.services.adyen.com'
|
71
|
+
end
|
72
|
+
|
73
|
+
# Set endpoint URI before dispatch, so that changes in environment
|
74
|
+
# are reflected correctly.
|
75
|
+
def on_before_dispatch
|
76
|
+
self.class.endpoint(:uri => self.class::ENDPOINT_URI % Adyen.environment.to_s, :version => 1)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# SOAP client to interact with the payment modification service of Adyen.
|
81
|
+
# At this moment, none of the calls are implemented.
|
82
|
+
class PaymentService < Base
|
83
|
+
|
84
|
+
ENDPOINT_URI = 'https://pal-%s.adyen.com/pal/servlet/soap/Payment'
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
# SOAP client to interact with the recurring payment service of Adyen.
|
89
|
+
# This client implements the submitRecurring call to submit payments
|
90
|
+
# for a recurring contract. Moreover, it implements the deactiveRecurring
|
91
|
+
# call to cancel a recurring contract.
|
92
|
+
#
|
93
|
+
# See the Adyen Recurring manual for more information about this SOAP service
|
94
|
+
class RecurringService < Base
|
95
|
+
|
96
|
+
ENDPOINT_URI = 'https://pal-%s.adyen.com/pal/servlet/soap/Recurring'
|
97
|
+
|
98
|
+
# Submits a recurring payment. Requires the following arguments as hash:
|
99
|
+
#
|
100
|
+
# * <tt>:currency</tt> The currency code (EUR, GBP, USD, etc)
|
101
|
+
# * <tt>:value</tt> The value of the payments in cents
|
102
|
+
# * <tt>:merchent_account</tt> The merchant account under which to place
|
103
|
+
# this payment.
|
104
|
+
# * <tt>:recurring_reference</tt> The psp_reference of the RECURRING_CONTRACT
|
105
|
+
# notification that was sent after the initial payment.
|
106
|
+
# * <tt>:reference</tt> The (merchant) reference for this payment.
|
107
|
+
# * <tt>:shopper_email</tt> The email address of the shopper.
|
108
|
+
# * <tt>:shopper_reference</tt> The refrence of the shopper. This should be
|
109
|
+
# the same as the reference that was used to create the recurring contract.
|
110
|
+
def submit(args = {})
|
111
|
+
invoke_args = Adyen::SOAP.default_arguments.merge(args)
|
112
|
+
response = invoke('recurring:submitRecurring') do |message|
|
113
|
+
message.add('recurring:recurringRequest') do |req|
|
114
|
+
req.add('recurring:amount') do |amount|
|
115
|
+
amount.add('common:currency', invoke_args[:currency])
|
116
|
+
amount.add('common:value', invoke_args[:value])
|
117
|
+
end
|
118
|
+
req.add('recurring:merchantAccount', invoke_args[:merchant_account])
|
119
|
+
req.add('recurring:recurringReference', invoke_args[:recurring_reference])
|
120
|
+
req.add('recurring:reference', invoke_args[:reference])
|
121
|
+
req.add('recurring:shopperEmail', invoke_args[:shopper_email])
|
122
|
+
req.add('recurring:shopperReference', invoke_args[:shopper_reference])
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Deactivates a recurring payment contract. Requires the following arguments:
|
128
|
+
#
|
129
|
+
# * <tt>:merchent_account</tt> The merchant account under which to place
|
130
|
+
# this payment.
|
131
|
+
# * <tt>:recurring_reference</tt> The psp_reference of the RECURRING_CONTRACT
|
132
|
+
# notification that was sent after the initial payment.
|
133
|
+
# * <tt>:reference</tt> The (merchant) reference for this deactivation.
|
134
|
+
# * <tt>:shopper_reference</tt> The refrence of the shopper. This should be
|
135
|
+
# the same as the reference that was used to create the recurring contract.
|
136
|
+
def deactivate(args = {})
|
137
|
+
invoke_args = Adyen::SOAP.default_arguments.merge(args)
|
138
|
+
response = invoke('recurring:deactivateRecurring') do |message|
|
139
|
+
message.add('recurring:recurringRequest') do |req|
|
140
|
+
req.add('recurring:merchantAccount', invoke_args[:merchant_account])
|
141
|
+
req.add('recurring:recurringReference', invoke_args[:recurring_reference])
|
142
|
+
req.add('recurring:reference', invoke_args[:reference])
|
143
|
+
req.add('recurring:shopperReference', invoke_args[:shopper_reference])
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
data/spec/adyen_spec.rb
ADDED
@@ -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
|
data/spec/form_spec.rb
CHANGED
@@ -28,6 +28,41 @@ describe Adyen::Form do
|
|
28
28
|
end
|
29
29
|
end
|
30
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
|
+
|
31
66
|
describe 'hidden fields generation' do
|
32
67
|
|
33
68
|
include ActionView::Helpers::TagHelper
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/spec_helper.rb"
|
2
|
+
|
3
|
+
require 'action_controller'
|
4
|
+
require 'action_controller/test_process'
|
5
|
+
|
6
|
+
describe Adyen::Notification do
|
7
|
+
|
8
|
+
before(:all) do
|
9
|
+
ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:')
|
10
|
+
|
11
|
+
ActiveRecord::Migration.verbose = false
|
12
|
+
Adyen::Notification::Migration.up
|
13
|
+
end
|
14
|
+
|
15
|
+
after(:all) do
|
16
|
+
Adyen::Notification::Migration.down
|
17
|
+
end
|
18
|
+
|
19
|
+
describe Adyen::Notification::HttpPost do
|
20
|
+
|
21
|
+
describe 'receiving payment authorization notification' do
|
22
|
+
|
23
|
+
before(:each) do
|
24
|
+
@request = mock('request')
|
25
|
+
@request.stub!(:params).and_return({
|
26
|
+
"merchantAccountCode"=>"FloorPlannerNL", "eventCode"=>"AUTHORISATION",
|
27
|
+
"paymentMethod"=>"mc", "eventDate"=>"2009-08-10T09:00:08.04Z",
|
28
|
+
"operations"=>"CANCEL,CAPTURE,REFUND", "merchantReference"=>"4",
|
29
|
+
"action"=>"process_adyen", "live"=>"false", "controller"=>"payment_notifications",
|
30
|
+
"value"=>"2500", "success"=>"false", "reason"=>"10676:1111:12/2012",
|
31
|
+
"originalReference"=>"", "pspReference"=>"8712498948081194", "currency"=>"USD"})
|
32
|
+
|
33
|
+
@notification = Adyen::Notification::HttpPost.log(@request)
|
34
|
+
end
|
35
|
+
|
36
|
+
after(:each) { @notification.destroy }
|
37
|
+
|
38
|
+
it "should have saved the notification record" do
|
39
|
+
@notification.should_not be_new_record
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should be an authorization" do
|
43
|
+
@notification.should be_authorisation
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should convert the amount to a bigdecimal" do
|
47
|
+
@notification.value.should eql(BigDecimal.new('25.00'))
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should convert live to a boolean" do
|
51
|
+
@notification.should_not be_live
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should convert success to a boolean" do
|
55
|
+
@notification.should_not be_success
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should not be a successfull authorization" do
|
59
|
+
@notification.should_not be_successful_authorization
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should convert the eventDate" do
|
63
|
+
@notification.event_date.should be_kind_of(Time)
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should convert the empty original reference to NULL" do
|
67
|
+
@notification.original_reference.should be_nil
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
context 'duplicate detection' do
|
72
|
+
before(:each) do
|
73
|
+
|
74
|
+
@fields = { "merchantAccountCode"=>"FloorPlannerNL", "eventCode"=>"AUTHORISATION",
|
75
|
+
"paymentMethod"=>"mc", "eventDate"=>"2009-08-10T09:00:08.04Z",
|
76
|
+
"operations"=>"CANCEL,CAPTURE,REFUND", "merchantReference"=>"4",
|
77
|
+
"action"=>"process_adyen", "live"=>"false", "controller"=>"payment_notifications",
|
78
|
+
"value"=>"2500", "success"=>"false", "reason"=>"10676:1111:12/2012",
|
79
|
+
"originalReference"=>"", "pspReference"=>"8712498948081194", "currency"=>"USD"}
|
80
|
+
|
81
|
+
@request = mock('request')
|
82
|
+
@request.stub!(:params).and_return(@fields)
|
83
|
+
@notification = Adyen::Notification::HttpPost.log(@request)
|
84
|
+
end
|
85
|
+
|
86
|
+
after(:each) { @notification.destroy }
|
87
|
+
|
88
|
+
it "should raise an error on a duplicate notification" do
|
89
|
+
lambda { Adyen::Notification::HttpPost.log(@request) }.should raise_error(ActiveRecord::RecordInvalid)
|
90
|
+
end
|
91
|
+
|
92
|
+
it "should not raise an error on a when success is set to true" do
|
93
|
+
second_request = mock('request')
|
94
|
+
second_request.stub!(:params).and_return(@fields.merge('success' => 'true'))
|
95
|
+
lambda { Adyen::Notification::HttpPost.log(second_request) }.should_not raise_error
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
data/spec/soap_spec.rb
ADDED
data/tasks/github-gem.rake
CHANGED
@@ -26,27 +26,37 @@ module Rake
|
|
26
26
|
desc "Updates the file lists for this gem"
|
27
27
|
task(:manifest) { manifest_task }
|
28
28
|
|
29
|
-
desc "
|
29
|
+
desc "Builds a gem package for #{@name}"
|
30
30
|
task(:build => [:manifest]) { build_task }
|
31
31
|
|
32
32
|
|
33
33
|
release_dependencies = [:check_clean_master_branch, :version, :build, :create_tag]
|
34
|
-
release_dependencies.push 'doc:publish' if
|
34
|
+
release_dependencies.push 'doc:publish' if has_rdoc?
|
35
35
|
release_dependencies.unshift 'test' if has_tests?
|
36
36
|
release_dependencies.unshift 'spec' if has_specs?
|
37
37
|
|
38
38
|
desc "Releases a new version of #{@name}"
|
39
39
|
task(:release => release_dependencies) { release_task }
|
40
40
|
|
41
|
+
namespace(:release) do
|
42
|
+
release_checks = [:check_clean_master_branch, :check_version, :build]
|
43
|
+
release_checks.push 'doc:compile' if has_rdoc?
|
44
|
+
release_checks.unshift 'test' if has_tests?
|
45
|
+
release_checks.unshift 'spec' if has_specs?
|
46
|
+
|
47
|
+
desc "Test release conditions"
|
48
|
+
task(:check => release_checks) { release_check_task }
|
49
|
+
end
|
50
|
+
|
41
51
|
# helper task for releasing
|
42
|
-
task(:check_clean_master_branch) { verify_clean_status('master') }
|
52
|
+
task(:check_clean_master_branch) { verify_fast_forward('master', 'origin', 'master'); verify_clean_status('master') }
|
43
53
|
task(:check_version) { verify_version(ENV['VERSION'] || @specification.version) }
|
44
54
|
task(:version => [:check_version]) { set_gem_version! }
|
45
55
|
task(:create_tag) { create_version_tag! }
|
46
56
|
end
|
47
57
|
|
48
58
|
# Register RDoc tasks
|
49
|
-
if
|
59
|
+
if has_rdoc?
|
50
60
|
require 'rake/rdoctask'
|
51
61
|
|
52
62
|
namespace(:doc) do
|
@@ -77,7 +87,7 @@ module Rake
|
|
77
87
|
|
78
88
|
desc "Run all specs for #{@name}"
|
79
89
|
Spec::Rake::SpecTask.new(:spec) do |t|
|
80
|
-
t.spec_files = FileList[
|
90
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
81
91
|
end
|
82
92
|
end
|
83
93
|
|
@@ -87,7 +97,7 @@ module Rake
|
|
87
97
|
|
88
98
|
desc "Run all unit tests for #{@name}"
|
89
99
|
Rake::TestTask.new(:test) do |t|
|
90
|
-
t.pattern =
|
100
|
+
t.pattern = 'test/**/*_test.rb'
|
91
101
|
t.verbose = true
|
92
102
|
t.libs << 'test'
|
93
103
|
end
|
@@ -96,16 +106,16 @@ module Rake
|
|
96
106
|
|
97
107
|
protected
|
98
108
|
|
99
|
-
def
|
100
|
-
|
109
|
+
def has_rdoc?
|
110
|
+
run_command('git branch').any? { |line| /gh-pages\s*$/ =~ line}
|
101
111
|
end
|
102
112
|
|
103
113
|
def has_specs?
|
104
|
-
Dir[
|
114
|
+
Dir['spec/**/*_spec.rb'].any?
|
105
115
|
end
|
106
116
|
|
107
117
|
def has_tests?
|
108
|
-
Dir[
|
118
|
+
Dir['test/**/*_test.rb'].any?
|
109
119
|
end
|
110
120
|
|
111
121
|
def reload_gemspec!
|
@@ -161,23 +171,22 @@ module Rake
|
|
161
171
|
end
|
162
172
|
|
163
173
|
def gemspec_file
|
164
|
-
@gemspec_file ||= Dir[
|
165
|
-
end
|
166
|
-
|
167
|
-
def git_branch_exists?(branch_name)
|
168
|
-
branches = run_command('git branch').map { |line| /^\*?\s+(\w+)/ =~ line; $1 }
|
169
|
-
branches.include?(branch_name.to_s)
|
174
|
+
@gemspec_file ||= Dir['*.gemspec'].first
|
170
175
|
end
|
171
176
|
|
172
177
|
def verify_current_branch(branch)
|
173
178
|
run_command('git branch').detect { |line| /^\* (.+)/ =~ line }
|
174
|
-
raise "You are currently not working in the
|
179
|
+
raise "You are currently not working in the #{branch} branch!" unless branch == $1
|
175
180
|
end
|
176
181
|
|
177
|
-
def
|
178
|
-
sh "git fetch"
|
182
|
+
def verify_fast_forward(local_branch = 'master', remote = 'origin', remote_branch = 'master')
|
183
|
+
sh "git fetch #{remote}"
|
184
|
+
lines = run_command("git rev-list #{local_branch}..remotes/#{remote}/#{remote_branch}")
|
185
|
+
raise "Remote branch #{remote}/#{remote_branch} has commits that are not yet incorporated in local #{local_branch} branch" unless lines.length == 0
|
186
|
+
end
|
187
|
+
|
188
|
+
def verify_clean_status(on_branch = nil)
|
179
189
|
lines = run_command('git status')
|
180
|
-
raise "You don't have the most recent version available. Run git pull first." if /^\# Your branch is behind/ =~ lines[1]
|
181
190
|
raise "You are currently not working in the #{on_branch} branch!" unless on_branch.nil? || (/^\# On branch (.+)/ =~ lines.first && $1 == on_branch)
|
182
191
|
raise "Your master branch contains modifications!" unless /^nothing to commit \(working directory clean\)/ =~ lines.last
|
183
192
|
end
|
@@ -249,7 +258,15 @@ module Rake
|
|
249
258
|
puts '------------------------------------------------------------'
|
250
259
|
puts "Released #{@name} - version #{@specification.version}"
|
251
260
|
end
|
261
|
+
|
262
|
+
def release_check_task
|
263
|
+
puts
|
264
|
+
puts '------------------------------------------------------------'
|
265
|
+
puts "Checked all conditions for a release of version #{ENV['VERSION'] || @specification.version}!"
|
266
|
+
puts 'You should be safe to do a release now.'
|
267
|
+
puts '------------------------------------------------------------'
|
268
|
+
end
|
252
269
|
end
|
253
270
|
end
|
254
271
|
|
255
|
-
Rake::GithubGem.define_tasks!
|
272
|
+
Rake::GithubGem.define_tasks!
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: wvanbergen-adyen
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Willem van Bergen
|
@@ -10,7 +10,7 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date: 2009-08-
|
13
|
+
date: 2009-08-20 00:00:00 -07:00
|
14
14
|
default_executable:
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
@@ -41,10 +41,17 @@ files:
|
|
41
41
|
- lib
|
42
42
|
- lib/adyen
|
43
43
|
- lib/adyen.rb
|
44
|
+
- lib/adyen/encoding.rb
|
44
45
|
- lib/adyen/form.rb
|
46
|
+
- lib/adyen/formatter.rb
|
45
47
|
- lib/adyen/matchers.rb
|
48
|
+
- lib/adyen/notification.rb
|
49
|
+
- lib/adyen/soap.rb
|
46
50
|
- spec
|
51
|
+
- spec/adyen_spec.rb
|
47
52
|
- spec/form_spec.rb
|
53
|
+
- spec/notification_spec.rb
|
54
|
+
- spec/soap_spec.rb
|
48
55
|
- spec/spec_helper.rb
|
49
56
|
- tasks
|
50
57
|
- tasks/github-gem.rake
|
@@ -81,4 +88,7 @@ signing_key:
|
|
81
88
|
specification_version: 2
|
82
89
|
summary: Integrate Adyen payment services in you Ruby on Rails application
|
83
90
|
test_files:
|
91
|
+
- spec/adyen_spec.rb
|
84
92
|
- spec/form_spec.rb
|
93
|
+
- spec/notification_spec.rb
|
94
|
+
- spec/soap_spec.rb
|