floorplanner-adyen 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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