adyen 0.1.4

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 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,32 @@
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 your client/customer:
6
+
7
+ * Client-to-Adyen communication using forms and redirects.
8
+ * Adyen-to-server communications using notifications.
9
+ * Server-to-Adyen communication using SOAP services.
10
+
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.
12
+
13
+ == Installation
14
+
15
+ This plugin can either be installed as gem or Rails plugin:
16
+
17
+ gem install wvanbergen-adyen --source http://gems.github.com # as gem
18
+ script/plugin install git://github.com/wvanbergen/adyen.git # as plugin
19
+
20
+ == Usage
21
+
22
+ See the project wiki on http://wiki.github.com/wvanbergen/adyen to get started.
23
+
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
27
+
28
+ == About
29
+
30
+ This package is written by Michel Barbosa and Willem van Bergen for Floorplanner.com,
31
+ and made public under the MIT license (see LICENSE). It comes without warranty of any kind,
32
+ so 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,21 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'adyen'
3
+ s.version = "0.1.4"
4
+ s.date = "2009-09-29"
5
+
6
+ s.summary = "Integrate Adyen payment services in you Ruby on Rails application"
7
+ s.description = "Package to simplify including the Adyen payments services into a Ruby on Rails application."
8
+
9
+ s.authors = ['Willem van Bergen', 'Michel Barbosa']
10
+ s.email = ['willem@vanbergen.org', 'cicaboo@gmail.com']
11
+ s.homepage = 'http://www.adyen.com'
12
+
13
+ s.add_development_dependency('rspec', '>= 1.1.4')
14
+ s.add_development_dependency('git', '>= 1.1.0')
15
+
16
+ s.rdoc_options << '--title' << s.name << '--main' << 'README.rdoc' << '--line-numbers' << '--inline-source'
17
+ s.extra_rdoc_files = ['README.rdoc']
18
+
19
+ 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)
20
+ s.test_files = %w(spec/soap_spec.rb spec/notification_spec.rb spec/adyen_spec.rb spec/form_spec.rb)
21
+ 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,71 @@
1
+ require 'action_view'
2
+
3
+ module Adyen
4
+ module Form
5
+
6
+ extend ActionView::Helpers::TagHelper
7
+
8
+ ACTION_URL = "https://%s.adyen.com/hpp/select.shtml"
9
+
10
+ def self.url(environment = nil)
11
+ environment ||= Adyen.environment(environment)
12
+ Adyen::Form::ACTION_URL % environment.to_s
13
+ end
14
+
15
+ def self.calculate_signature_string(attributes)
16
+ merchant_sig_string = ""
17
+ merchant_sig_string << attributes[:payment_amount].to_s << attributes[:currency_code].to_s <<
18
+ attributes[:ship_before_date].to_s << attributes[:merchant_reference].to_s <<
19
+ attributes[:skin_code].to_s << attributes[:merchant_account].to_s <<
20
+ attributes[:session_validity].to_s << attributes[:shopper_email].to_s <<
21
+ attributes[:shopper_reference].to_s << attributes[:recurring_contract].to_s <<
22
+ attributes[:allowed_methods].to_s << attributes[:blocked_methods].to_s <<
23
+ attributes[:shopper_statement].to_s << attributes[:billing_address_type].to_s
24
+ end
25
+
26
+ def self.calculate_signature(attributes)
27
+ Adyen::Encoding.hmac_base64(attributes.delete(:shared_secret), calculate_signature_string(attributes))
28
+ end
29
+
30
+ def self.do_attribute_transformations!(attributes = {})
31
+ raise "YENs are not yet supported!" if attributes[:currency_code] == 'JPY' # TODO: fixme
32
+
33
+ attributes[:recurring_contract] = 'DEFAULT' if attributes.delete(:recurring) == true
34
+ attributes[:order_data] = Adyen::Encoding.gzip_base64(attributes.delete(:order_data_raw)) if attributes[:order_data_raw]
35
+ attributes[:ship_before_date] = Adyen::Formatter::DateTime.fmt_date(attributes[:ship_before_date])
36
+ attributes[:session_validity] = Adyen::Formatter::DateTime.fmt_time(attributes[:session_validity])
37
+ end
38
+
39
+
40
+ def self.hidden_fields(attributes = {})
41
+ do_attribute_transformations!(attributes)
42
+
43
+ raise "Cannot generate form: :currency code attribute not found!" unless attributes[:currency_code]
44
+ raise "Cannot generate form: :payment_amount code attribute not found!" unless attributes[:payment_amount]
45
+ raise "Cannot generate form: :merchant_account attribute not found!" unless attributes[:merchant_account]
46
+ raise "Cannot generate form: :skin_code attribute not found!" unless attributes[:skin_code]
47
+ raise "Cannot generate form: :shared_secret signing secret not provided!" unless attributes[:shared_secret]
48
+
49
+ # Merchant signature
50
+ attributes[:merchant_sig] = calculate_signature(attributes)
51
+
52
+ # Generate hidden input tags
53
+ attributes.map { |key, value|
54
+ self.tag(:input, :type => 'hidden', :name => key.to_s.camelize(:lower), :value => value)
55
+ }.join("\n")
56
+ end
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
+
70
+ end
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,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