floorplanner-adyen 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4 @@
1
+ /tmp
2
+ /pkg
3
+ /doc
4
+ adyen-*.gem
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.
@@ -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.
@@ -0,0 +1,5 @@
1
+ Dir[File.dirname(__FILE__) + "/tasks/*.rake"].each { |file| load(file) }
2
+
3
+ GithubGem::RakeTasks.new(:gem)
4
+
5
+ task :default => "spec:specdoc"
@@ -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'
@@ -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
@@ -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