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.
- data/.gitignore +4 -0
- data/LICENSE +20 -0
- data/README.rdoc +39 -0
- data/Rakefile +5 -0
- data/adyen.gemspec +30 -0
- data/init.rb +1 -0
- data/lib/adyen.rb +31 -0
- data/lib/adyen/encoding.rb +21 -0
- data/lib/adyen/form.rb +138 -0
- data/lib/adyen/formatter.rb +37 -0
- data/lib/adyen/matchers.rb +105 -0
- data/lib/adyen/notification.rb +99 -0
- data/lib/adyen/soap.rb +153 -0
- data/spec/adyen_spec.rb +53 -0
- data/spec/form_spec.rb +152 -0
- data/spec/notification_spec.rb +97 -0
- data/spec/soap_spec.rb +5 -0
- data/spec/spec_helper.rb +11 -0
- data/tasks/github-gem.rake +323 -0
- metadata +105 -0
data/.gitignore
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright (c) 2008 - 2009 Willem van Bergen and Michel Barbosa
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
= Adyen
|
|
2
|
+
|
|
3
|
+
Package to simplify including the Adyen payments services into a Ruby on Rails application.
|
|
4
|
+
|
|
5
|
+
Adyen integration relies on three modes of communication between Adyen, your server and
|
|
6
|
+
your client/customer:
|
|
7
|
+
|
|
8
|
+
* Client-to-Adyen communication using forms and redirects.
|
|
9
|
+
* Adyen-to-server communications using notifications.
|
|
10
|
+
* Server-to-Adyen communication using SOAP services.
|
|
11
|
+
|
|
12
|
+
This library aims to ease the implementation of all these modes into your application.
|
|
13
|
+
Moreover, it provides matchers, assertions and mocks to make it easier to implement an
|
|
14
|
+
automated test suite to assert the integration is working correctly.
|
|
15
|
+
|
|
16
|
+
== Installation
|
|
17
|
+
|
|
18
|
+
Add the following line to your <tt>environment.rb</tt> and run <tt>rake gems:install</tt>
|
|
19
|
+
to make the Adyen functionality available in your Rails project:
|
|
20
|
+
|
|
21
|
+
config.gem 'adyen', :source => 'http://gemcutter.org
|
|
22
|
+
|
|
23
|
+
You can also install it as a Rails plugin (*deprecated*):
|
|
24
|
+
|
|
25
|
+
script/plugin install git://github.com/wvanbergen/adyen.git
|
|
26
|
+
|
|
27
|
+
== Usage
|
|
28
|
+
|
|
29
|
+
See the project wiki on http://wiki.github.com/wvanbergen/adyen to get started.
|
|
30
|
+
|
|
31
|
+
* For more information about Adyen, see http://www.adyen.com
|
|
32
|
+
* For more information about integrating Adyen, see their manuals at
|
|
33
|
+
http://support.adyen.com/links/documentation
|
|
34
|
+
|
|
35
|
+
== About
|
|
36
|
+
|
|
37
|
+
This package is written by Michel Barbosa and Willem van Bergen for Floorplanner.com, and
|
|
38
|
+
made public under the MIT license (see LICENSE). It comes without warranty of any kind, so
|
|
39
|
+
use at your own risk.
|
data/Rakefile
ADDED
data/adyen.gemspec
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Gem::Specification.new do |s|
|
|
2
|
+
s.name = 'floorplanner-adyen'
|
|
3
|
+
s.version = "0.2.2"
|
|
4
|
+
s.date = "2009-11-17"
|
|
5
|
+
|
|
6
|
+
s.summary = "Integrate Adyen payment services in you Ruby on Rails application."
|
|
7
|
+
s.description = <<-EOS
|
|
8
|
+
Package to simplify including the Adyen payments services into a Ruby on Rails application.
|
|
9
|
+
The package provides functionality to create payment forms, handling and storing notifications
|
|
10
|
+
sent by Adyen and consuming the SOAP services provided by Adyen. Moreover, it contains helper
|
|
11
|
+
methods, mocks and matchers to simpify writing tests/specsfor your code.
|
|
12
|
+
EOS
|
|
13
|
+
|
|
14
|
+
s.authors = ['Willem van Bergen', 'Michel Barbosa']
|
|
15
|
+
s.email = ['willem@vanbergen.org', 'cicaboo@gmail.com']
|
|
16
|
+
s.homepage = 'http://wiki.github.com/wvanbergen/adyen'
|
|
17
|
+
|
|
18
|
+
s.add_development_dependency('rspec', '>= 1.1.4')
|
|
19
|
+
s.add_development_dependency('git', '>= 1.1.0')
|
|
20
|
+
|
|
21
|
+
s.requirements << 'Handsoap is required for accessing the SOAP services. See http://github.com/troelskn/handsoap.'
|
|
22
|
+
s.requirements << 'LibXML is required for using the RSpec matchers.'
|
|
23
|
+
s.requirements << 'ActiveRecord is required for storing the notifications in your database.'
|
|
24
|
+
|
|
25
|
+
s.rdoc_options << '--title' << s.name << '--main' << 'README.rdoc' << '--line-numbers' << '--inline-source'
|
|
26
|
+
s.extra_rdoc_files = ['README.rdoc']
|
|
27
|
+
|
|
28
|
+
s.files = %w(spec/spec_helper.rb lib/adyen/form.rb .gitignore LICENSE spec/soap_spec.rb spec/notification_spec.rb lib/adyen/soap.rb init.rb spec/adyen_spec.rb adyen.gemspec Rakefile tasks/github-gem.rake spec/form_spec.rb README.rdoc lib/adyen/notification.rb lib/adyen/matchers.rb lib/adyen/formatter.rb lib/adyen.rb lib/adyen/encoding.rb)
|
|
29
|
+
s.test_files = %w(spec/soap_spec.rb spec/notification_spec.rb spec/adyen_spec.rb spec/form_spec.rb)
|
|
30
|
+
end
|
data/init.rb
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require 'adyen'
|
data/lib/adyen.rb
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Adyen
|
|
2
|
+
LIVE_RAILS_ENVIRONMENTS = ['production']
|
|
3
|
+
|
|
4
|
+
# Setter voor the current Adyen environment.
|
|
5
|
+
# Must be either 'test' or 'live'
|
|
6
|
+
def self.environment=(env)
|
|
7
|
+
@environment = env
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Returns the current Adyen environment.
|
|
11
|
+
# Returns either 'test' or 'live'.
|
|
12
|
+
def self.environment(override = nil)
|
|
13
|
+
override || @environment || Adyen.autodetect_environment
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Autodetects the Adyen environment based on the RAILS_ENV constant
|
|
17
|
+
def self.autodetect_environment
|
|
18
|
+
(defined?(RAILS_ENV) && Adyen::LIVE_RAILS_ENVIRONMENTS.include?(RAILS_ENV.to_s.downcase)) ? 'live' : 'test'
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Loads submodules on demand, so that dependencies are not required.
|
|
22
|
+
def self.const_missing(sym)
|
|
23
|
+
require "adyen/#{sym.to_s.downcase}"
|
|
24
|
+
return Adyen.const_get(sym)
|
|
25
|
+
rescue
|
|
26
|
+
super(sym)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
require 'adyen/encoding'
|
|
31
|
+
require 'adyen/formatter'
|
|
@@ -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
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
require 'action_view'
|
|
2
|
+
|
|
3
|
+
module Adyen
|
|
4
|
+
module Form
|
|
5
|
+
|
|
6
|
+
extend ActionView::Helpers::TagHelper
|
|
7
|
+
|
|
8
|
+
######################################################
|
|
9
|
+
# SKINS
|
|
10
|
+
######################################################
|
|
11
|
+
|
|
12
|
+
def self.skins
|
|
13
|
+
@skins ||= {}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.register_skin(name, skin_code, shared_secret)
|
|
17
|
+
self.skins[name] = {:name => name, :skin_code => skin_code, :shared_secret => shared_secret }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.skin_by_name(skin_name)
|
|
21
|
+
self.skins[skin_name]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.skin_by_code(skin_code)
|
|
25
|
+
self.skins.detect { |(name, skin)| skin[:skin_code] == skin_code }.last rescue nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.lookup_shared_secret(skin_code)
|
|
29
|
+
skin = skin_by_code(skin_code)[:shared_secret] rescue nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
######################################################
|
|
33
|
+
# DEFAULT FORM / REDIRECT PARAMETERS
|
|
34
|
+
######################################################
|
|
35
|
+
|
|
36
|
+
def self.default_parameters
|
|
37
|
+
@default_arguments ||= {}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.default_parameters=(hash)
|
|
41
|
+
@default_arguments = hash
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
######################################################
|
|
45
|
+
# ADYEN FORM URL
|
|
46
|
+
######################################################
|
|
47
|
+
|
|
48
|
+
ACTION_URL = "https://%s.adyen.com/hpp/select.shtml"
|
|
49
|
+
|
|
50
|
+
def self.url(environment = nil)
|
|
51
|
+
environment ||= Adyen.environment(environment)
|
|
52
|
+
Adyen::Form::ACTION_URL % environment.to_s
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
######################################################
|
|
57
|
+
# POSTING/REDIRECTING TO ADYEN
|
|
58
|
+
######################################################
|
|
59
|
+
|
|
60
|
+
def self.do_parameter_transformations!(parameters = {})
|
|
61
|
+
raise "YENs are not yet supported!" if parameters[:currency_code] == 'JPY' # TODO: fixme
|
|
62
|
+
|
|
63
|
+
parameters.replace(default_parameters.merge(parameters))
|
|
64
|
+
parameters[:recurring_contract] = 'DEFAULT' if parameters.delete(:recurring) == true
|
|
65
|
+
parameters[:order_data] = Adyen::Encoding.gzip_base64(parameters.delete(:order_data_raw)) if parameters[:order_data_raw]
|
|
66
|
+
parameters[:ship_before_date] = Adyen::Formatter::DateTime.fmt_date(parameters[:ship_before_date])
|
|
67
|
+
parameters[:session_validity] = Adyen::Formatter::DateTime.fmt_time(parameters[:session_validity])
|
|
68
|
+
|
|
69
|
+
if parameters[:skin]
|
|
70
|
+
skin = Adyen::Form.skin_by_name(parameters.delete(:skin))
|
|
71
|
+
parameters[:skin_code] ||= skin[:skin_code]
|
|
72
|
+
parameters[:shared_secret] ||= skin[:shared_secret]
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def self.payment_parameters(parameters = {})
|
|
77
|
+
do_parameter_transformations!(parameters)
|
|
78
|
+
|
|
79
|
+
raise "Cannot generate form: :currency code attribute not found!" unless parameters[:currency_code]
|
|
80
|
+
raise "Cannot generate form: :payment_amount code attribute not found!" unless parameters[:payment_amount]
|
|
81
|
+
raise "Cannot generate form: :merchant_account attribute not found!" unless parameters[:merchant_account]
|
|
82
|
+
raise "Cannot generate form: :skin_code attribute not found!" unless parameters[:skin_code]
|
|
83
|
+
raise "Cannot generate form: :shared_secret signing secret not provided!" unless parameters[:shared_secret]
|
|
84
|
+
|
|
85
|
+
# Merchant signature
|
|
86
|
+
parameters[:merchant_sig] = calculate_signature(parameters)
|
|
87
|
+
return parameters
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def self.redirect_url(parameters = {})
|
|
91
|
+
self.url + '?' + payment_parameters(parameters).map { |(k, v)| "#{k.to_s.camelize(:lower)}=#{CGI.escape(v.to_s)}" }.join('&')
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def self.hidden_fields(parameters = {})
|
|
95
|
+
# Generate hidden input tags
|
|
96
|
+
payment_parameters(parameters).map { |key, value|
|
|
97
|
+
self.tag(:input, :type => 'hidden', :name => key.to_s.camelize(:lower), :value => value)
|
|
98
|
+
}.join("\n")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
######################################################
|
|
102
|
+
# MERCHANT SIGNATURE CALCULATION
|
|
103
|
+
######################################################
|
|
104
|
+
|
|
105
|
+
def self.calculate_signature_string(parameters)
|
|
106
|
+
merchant_sig_string = ""
|
|
107
|
+
merchant_sig_string << parameters[:payment_amount].to_s << parameters[:currency_code].to_s <<
|
|
108
|
+
parameters[:ship_before_date].to_s << parameters[:merchant_reference].to_s <<
|
|
109
|
+
parameters[:skin_code].to_s << parameters[:merchant_account].to_s <<
|
|
110
|
+
parameters[:session_validity].to_s << parameters[:shopper_email].to_s <<
|
|
111
|
+
parameters[:shopper_reference].to_s << parameters[:recurring_contract].to_s <<
|
|
112
|
+
parameters[:allowed_methods].to_s << parameters[:blocked_methods].to_s <<
|
|
113
|
+
parameters[:shopper_statement].to_s << parameters[:billing_address_type].to_s
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def self.calculate_signature(parameters)
|
|
117
|
+
Adyen::Encoding.hmac_base64(parameters.delete(:shared_secret), calculate_signature_string(parameters))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
######################################################
|
|
121
|
+
# REDIRECT SIGNATURE CHECKING
|
|
122
|
+
######################################################
|
|
123
|
+
|
|
124
|
+
def self.redirect_signature_string(params)
|
|
125
|
+
params[:authResult].to_s + params[:pspReference].to_s + params[:merchantReference].to_s + params[:skinCode].to_s
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def self.redirect_signature(params, shared_secret = nil)
|
|
129
|
+
shared_secret ||= lookup_shared_secret(params[:skinCode])
|
|
130
|
+
Adyen::Encoding.hmac_base64(shared_secret, redirect_signature_string(params))
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def self.redirect_signature_check(params, shared_secret = nil)
|
|
134
|
+
params[:merchantSig] == redirect_signature(params, shared_secret)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
end
|
|
138
|
+
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,105 @@
|
|
|
1
|
+
require 'xml'
|
|
2
|
+
|
|
3
|
+
module Adyen
|
|
4
|
+
module Matchers
|
|
5
|
+
|
|
6
|
+
module XPathPaymentFormCheck
|
|
7
|
+
|
|
8
|
+
def self.build_xpath_query(checks)
|
|
9
|
+
# Start by finding the check for the Adyen form tag
|
|
10
|
+
xpath_query = "//form[@action='#{Adyen::Form.url}']"
|
|
11
|
+
|
|
12
|
+
# Add recurring/single check if specified
|
|
13
|
+
recurring = checks.delete(:recurring)
|
|
14
|
+
unless recurring.nil?
|
|
15
|
+
if recurring
|
|
16
|
+
xpath_query << "[descendant::input[@type='hidden'][@name='recurringContract']]"
|
|
17
|
+
else
|
|
18
|
+
xpath_query << "[not(descendant::input[@type='hidden'][@name='recurringContract'])]"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Add a check for all the other fields specified
|
|
23
|
+
checks.each do |key, value|
|
|
24
|
+
condition = "descendant::input[@type='hidden'][@name='#{key.to_s.camelize(:lower)}']"
|
|
25
|
+
condition << "[@value='#{value}']" unless value == :anything
|
|
26
|
+
xpath_query << "[#{condition}]"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
return xpath_query
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.document(subject)
|
|
33
|
+
if String === subject
|
|
34
|
+
XML::HTMLParser.string(subject).parse
|
|
35
|
+
elsif subject.respond_to?(:body)
|
|
36
|
+
XML::HTMLParser.string(subject.body).parse
|
|
37
|
+
elsif XML::Node === subject
|
|
38
|
+
subject
|
|
39
|
+
elsif XML::Document === subject
|
|
40
|
+
subject
|
|
41
|
+
else
|
|
42
|
+
raise "Cannot handle this XML input type"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.check(subject, checks = {})
|
|
47
|
+
document(subject).find_first(build_xpath_query(checks))
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
class HaveAdyenPaymentForm
|
|
52
|
+
|
|
53
|
+
def initialize(checks)
|
|
54
|
+
@checks = checks
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def matches?(document)
|
|
58
|
+
Adyen::Matchers::XPathPaymentFormCheck.check(document, @checks)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def description
|
|
62
|
+
"have an adyen payment form"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def failure_message
|
|
66
|
+
"expected to find a valid Adyen form on this page"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def negative_failure_message
|
|
70
|
+
"expected not to find a valid Adyen form on this page"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def have_adyen_payment_form(checks = {})
|
|
75
|
+
default_checks = {:merchant_sig => :anything, :payment_amount => :anything, :currency_code => :anything, :skin_code => :anything }
|
|
76
|
+
HaveAdyenPaymentForm.new(default_checks.merge(checks))
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def have_adyen_recurring_payment_form(checks = {})
|
|
80
|
+
recurring_checks = { :recurring => true, :shopper_email => :anything, :shopper_reference => :anything }
|
|
81
|
+
have_adyen_payment_form(recurring_checks.merge(checks))
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def have_adyen_single_payment_form(checks = {})
|
|
85
|
+
recurring_checks = { :recurring => false }
|
|
86
|
+
have_adyen_payment_form(recurring_checks.merge(checks))
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def assert_adyen_payment_form(subject, checks = {})
|
|
90
|
+
default_checks = {:merchant_sig => :anything, :payment_amount => :anything, :currency_code => :anything, :skin_code => :anything }
|
|
91
|
+
assert Adyen::Matchers::XPathPaymentFormCheck.check(subject, default_checks.merge(checks)), 'No Adyen payment form found'
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def assert_adyen_recurring_payment_form(subject, checks = {})
|
|
95
|
+
recurring_checks = { :recurring => true, :shopper_email => :anything, :shopper_reference => :anything }
|
|
96
|
+
assert_adyen_payment_form(subject, recurring_checks.merge(checks))
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def assert_adyen_single_payment_form(subject, checks = {})
|
|
100
|
+
recurring_checks = { :recurring => false }
|
|
101
|
+
assert_adyen_payment_form(subject, recurring_checks.merge(checks))
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
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, :name => 'adyen_notification_uniqueness'
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def self.down(table_name = Adyen::Notification::DEFAULT_TABLE_NAME)
|
|
94
|
+
remove_index(table_name, :name => 'adyen_notification_uniqueness')
|
|
95
|
+
drop_table(table_name)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|