wvanbergen-adyen 0.1.1 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE CHANGED
@@ -1,5 +1,5 @@
1
- Copyright (c) 2008 Willem van Bergen and Michel Barbosa
2
-
1
+ Copyright (c) 2008 - 2009 Willem van Bergen and Michel Barbosa
2
+
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
5
5
  "Software"), to deal in the Software without restriction, including
@@ -7,10 +7,10 @@ without limitation the rights to use, copy, modify, merge, publish,
7
7
  distribute, sublicense, and/or sell copies of the Software, and to
8
8
  permit persons to whom the Software is furnished to do so, subject to
9
9
  the following conditions:
10
-
10
+
11
11
  The above copyright notice and this permission notice shall be
12
12
  included in all copies or substantial portions of the Software.
13
-
13
+
14
14
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
15
  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
16
  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
data/README.rdoc CHANGED
@@ -2,20 +2,27 @@
2
2
 
3
3
  Package to simplify including the Adyen payments services into a Ruby on Rails application.
4
4
 
5
- Adyen integration relies on three modes of communication between Adyen, your server and your client/customer:
5
+ Adyen integration relies on three modes of communication between Adyen, your server and
6
+ your client/customer:
6
7
 
7
8
  * Client-to-Adyen communication using forms and redirects.
8
9
  * Adyen-to-server communications using notifications.
9
10
  * Server-to-Adyen communication using SOAP services.
10
11
 
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
+ 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.
12
15
 
13
16
  == Installation
14
17
 
15
- This plugin can either be installed as gem or Rails plugin:
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:
16
20
 
17
- gem install wvanbergen-adyen --source http://gems.github.com # as gem
18
- script/plugin install git://github.com/wvanbergen/adyen.git # as plugin
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
19
26
 
20
27
  == Usage
21
28
 
@@ -23,10 +30,10 @@ See the project wiki on http://wiki.github.com/wvanbergen/adyen to get started.
23
30
 
24
31
  * For more information about Adyen, see http://www.adyen.com
25
32
  * For more information about integrating Adyen, see their manuals at
26
- http://support.adyen.com/links/documentation
33
+ http://support.adyen.com/links/documentation
27
34
 
28
35
  == About
29
36
 
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.
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 CHANGED
@@ -1,5 +1,5 @@
1
1
  Dir[File.dirname(__FILE__) + "/tasks/*.rake"].each { |file| load(file) }
2
2
 
3
3
  GithubGem::RakeTasks.new(:gem)
4
-
4
+
5
5
  task :default => "spec:specdoc"
data/adyen.gemspec CHANGED
@@ -1,21 +1,30 @@
1
1
  Gem::Specification.new do |s|
2
- s.name = 'adyen'
3
- s.version = "0.1.1"
4
- s.date = "2009-09-03"
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
-
2
+ s.name = 'wvanbergen-adyen'
3
+ s.version = "0.1.5"
4
+ s.date = "2009-10-07"
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
+
9
14
  s.authors = ['Willem van Bergen', 'Michel Barbosa']
10
15
  s.email = ['willem@vanbergen.org', 'cicaboo@gmail.com']
11
- s.homepage = 'http://www.adyen.com'
12
-
16
+ s.homepage = 'http://wiki.github.com/wvanbergen/adyen'
17
+
13
18
  s.add_development_dependency('rspec', '>= 1.1.4')
14
- s.add_development_dependency('git', '>= 1.1.0')
15
-
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
+
16
25
  s.rdoc_options << '--title' << s.name << '--main' << 'README.rdoc' << '--line-numbers' << '--inline-source'
17
26
  s.extra_rdoc_files = ['README.rdoc']
18
-
27
+
19
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)
20
29
  s.test_files = %w(spec/soap_spec.rb spec/notification_spec.rb spec/adyen_spec.rb spec/form_spec.rb)
21
- end
30
+ end
data/lib/adyen.rb CHANGED
@@ -1,23 +1,23 @@
1
1
  module Adyen
2
- LIVE_RAILS_ENVIRONMENTS = ['production']
2
+ LIVE_RAILS_ENVIRONMENTS = ['production']
3
3
 
4
- # Setter voor the current Adyen environment.
4
+ # Setter voor the current Adyen environment.
5
5
  # Must be either 'test' or 'live'
6
6
  def self.environment=(env)
7
7
  @environment = env
8
8
  end
9
-
9
+
10
10
  # Returns the current Adyen environment.
11
11
  # Returns either 'test' or 'live'.
12
12
  def self.environment(override = nil)
13
13
  override || @environment || Adyen.autodetect_environment
14
- end
14
+ end
15
15
 
16
16
  # Autodetects the Adyen environment based on the RAILS_ENV constant
17
17
  def self.autodetect_environment
18
18
  (defined?(RAILS_ENV) && Adyen::LIVE_RAILS_ENVIRONMENTS.include?(RAILS_ENV.to_s.downcase)) ? 'live' : 'test'
19
19
  end
20
-
20
+
21
21
  # Loads submodules on demand, so that dependencies are not required.
22
22
  def self.const_missing(sym)
23
23
  require "adyen/#{sym.to_s.downcase}"
data/lib/adyen/form.rb CHANGED
@@ -2,70 +2,76 @@ require 'action_view'
2
2
 
3
3
  module Adyen
4
4
  module Form
5
-
5
+
6
6
  extend ActionView::Helpers::TagHelper
7
-
7
+
8
8
  ACTION_URL = "https://%s.adyen.com/hpp/select.shtml"
9
9
 
10
10
  def self.url(environment = nil)
11
11
  environment ||= Adyen.environment(environment)
12
12
  Adyen::Form::ACTION_URL % environment.to_s
13
13
  end
14
-
14
+
15
15
  def self.calculate_signature_string(attributes)
16
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 <<
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
19
  attributes[:skin_code].to_s << attributes[:merchant_account].to_s <<
20
20
  attributes[:session_validity].to_s << attributes[:shopper_email].to_s <<
21
21
  attributes[:shopper_reference].to_s << attributes[:recurring_contract].to_s <<
22
22
  attributes[:allowed_methods].to_s << attributes[:blocked_methods].to_s <<
23
23
  attributes[:shopper_statement].to_s << attributes[:billing_address_type].to_s
24
24
  end
25
-
25
+
26
26
  def self.calculate_signature(attributes)
27
27
  Adyen::Encoding.hmac_base64(attributes.delete(:shared_secret), calculate_signature_string(attributes))
28
28
  end
29
-
29
+
30
30
  def self.do_attribute_transformations!(attributes = {})
31
31
  raise "YENs are not yet supported!" if attributes[:currency_code] == 'JPY' # TODO: fixme
32
-
32
+
33
33
  attributes[:recurring_contract] = 'DEFAULT' if attributes.delete(:recurring) == true
34
34
  attributes[:order_data] = Adyen::Encoding.gzip_base64(attributes.delete(:order_data_raw)) if attributes[:order_data_raw]
35
35
  attributes[:ship_before_date] = Adyen::Formatter::DateTime.fmt_date(attributes[:ship_before_date])
36
36
  attributes[:session_validity] = Adyen::Formatter::DateTime.fmt_time(attributes[:session_validity])
37
37
  end
38
-
39
38
 
40
- def self.hidden_fields(attributes = {})
39
+ def self.payment_fields(attributes = {})
41
40
  do_attribute_transformations!(attributes)
42
-
41
+
43
42
  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]
43
+ raise "Cannot generate form: :payment_amount code attribute not found!" unless attributes[:payment_amount]
45
44
  raise "Cannot generate form: :merchant_account attribute not found!" unless attributes[:merchant_account]
46
45
  raise "Cannot generate form: :skin_code attribute not found!" unless attributes[:skin_code]
47
46
  raise "Cannot generate form: :shared_secret signing secret not provided!" unless attributes[:shared_secret]
48
-
47
+
49
48
  # Merchant signature
50
49
  attributes[:merchant_sig] = calculate_signature(attributes)
51
-
50
+ return attributes
51
+ end
52
+
53
+ def self.redirect_url(attributes)
54
+ self.url + '?' + payment_fields(attributes).map { |(k, v)| "#{k.to_s.camelize(:lower)}=#{CGI.escape(v.to_s)}" }.join('&')
55
+ end
56
+
57
+ def self.hidden_fields(attributes = {})
52
58
  # Generate hidden input tags
53
- attributes.map { |key, value|
59
+ payment_fields(attributes).map { |key, value|
54
60
  self.tag(:input, :type => 'hidden', :name => key.to_s.camelize(:lower), :value => value)
55
61
  }.join("\n")
56
62
  end
57
-
63
+
58
64
  def self.redirect_signature_string(params)
59
65
  params[:authResult].to_s + params[:pspReference].to_s + params[:merchantReference].to_s + params[:skinCode].to_s
60
66
  end
61
-
67
+
62
68
  def self.redirect_signature(params, shared_secret)
63
69
  Adyen::Encoding.hmac_base64(shared_secret, redirect_signature_string(params))
64
70
  end
65
-
71
+
66
72
  def self.redirect_signature_check(params, shared_secret)
67
73
  params[:merchantSig] == redirect_signature(params, shared_secret)
68
- end
69
-
74
+ end
75
+
70
76
  end
71
77
  end
@@ -28,7 +28,7 @@ module Adyen
28
28
  def self.in_cents(price)
29
29
  ((price * 100).round).to_i
30
30
  end
31
-
31
+
32
32
  def self.from_cents(price)
33
33
  BigDecimal.new(price.to_s) / 100
34
34
  end
@@ -1,19 +1,19 @@
1
1
  require 'xml'
2
2
 
3
3
  module Adyen
4
- module Matchers
5
-
4
+ module Matchers
5
+
6
6
  module XPathPaymentFormCheck
7
-
7
+
8
8
  def self.build_xpath_query(checks)
9
9
  # Start by finding the check for the Adyen form tag
10
- xpath_query = "//form[@action='#{Adyen::Form.url}']"
10
+ xpath_query = "//form[@action='#{Adyen::Form.url}']"
11
11
 
12
12
  # Add recurring/single check if specified
13
13
  recurring = checks.delete(:recurring)
14
14
  unless recurring.nil?
15
15
  if recurring
16
- xpath_query << "[descendant::input[@type='hidden'][@name='recurringContract']]"
16
+ xpath_query << "[descendant::input[@type='hidden'][@name='recurringContract']]"
17
17
  else
18
18
  xpath_query << "[not(descendant::input[@type='hidden'][@name='recurringContract'])]"
19
19
  end
@@ -24,9 +24,9 @@ module Adyen
24
24
  condition = "descendant::input[@type='hidden'][@name='#{key.to_s.camelize(:lower)}']"
25
25
  condition << "[@value='#{value}']" unless value == :anything
26
26
  xpath_query << "[#{condition}]"
27
- end
27
+ end
28
28
 
29
- return xpath_query
29
+ return xpath_query
30
30
  end
31
31
 
32
32
  def self.document(subject)
@@ -45,27 +45,27 @@ module Adyen
45
45
 
46
46
  def self.check(subject, checks = {})
47
47
  document(subject).find_first(build_xpath_query(checks))
48
- end
48
+ end
49
49
  end
50
-
50
+
51
51
  class HaveAdyenPaymentForm
52
-
52
+
53
53
  def initialize(checks)
54
54
  @checks = checks
55
55
  end
56
-
56
+
57
57
  def matches?(document)
58
58
  Adyen::Matchers::XPathPaymentFormCheck.check(document, @checks)
59
59
  end
60
-
60
+
61
61
  def description
62
62
  "have an adyen payment form"
63
63
  end
64
-
64
+
65
65
  def failure_message
66
66
  "expected to find a valid Adyen form on this page"
67
67
  end
68
-
68
+
69
69
  def negative_failure_message
70
70
  "expected not to find a valid Adyen form on this page"
71
71
  end
@@ -75,31 +75,31 @@ module Adyen
75
75
  default_checks = {:merchant_sig => :anything, :payment_amount => :anything, :currency_code => :anything, :skin_code => :anything }
76
76
  HaveAdyenPaymentForm.new(default_checks.merge(checks))
77
77
  end
78
-
78
+
79
79
  def have_adyen_recurring_payment_form(checks = {})
80
80
  recurring_checks = { :recurring => true, :shopper_email => :anything, :shopper_reference => :anything }
81
81
  have_adyen_payment_form(recurring_checks.merge(checks))
82
- end
83
-
82
+ end
83
+
84
84
  def have_adyen_single_payment_form(checks = {})
85
85
  recurring_checks = { :recurring => false }
86
86
  have_adyen_payment_form(recurring_checks.merge(checks))
87
87
  end
88
-
88
+
89
89
  def assert_adyen_payment_form(subject, checks = {})
90
90
  default_checks = {:merchant_sig => :anything, :payment_amount => :anything, :currency_code => :anything, :skin_code => :anything }
91
91
  assert Adyen::Matchers::XPathPaymentFormCheck.check(subject, default_checks.merge(checks)), 'No Adyen payment form found'
92
92
  end
93
-
93
+
94
94
  def assert_adyen_recurring_payment_form(subject, checks = {})
95
95
  recurring_checks = { :recurring => true, :shopper_email => :anything, :shopper_reference => :anything }
96
96
  assert_adyen_payment_form(subject, recurring_checks.merge(checks))
97
- end
97
+ end
98
98
 
99
99
  def assert_adyen_single_payment_form(subject, checks = {})
100
100
  recurring_checks = { :recurring => false }
101
101
  assert_adyen_payment_form(subject, recurring_checks.merge(checks))
102
- end
103
-
102
+ end
103
+
104
104
  end
105
105
  end
@@ -2,14 +2,14 @@ require 'activerecord'
2
2
 
3
3
  module Adyen
4
4
  class Notification < ActiveRecord::Base
5
-
5
+
6
6
  DEFAULT_TABLE_NAME = :adyen_notifications
7
7
  set_table_name(DEFAULT_TABLE_NAME)
8
-
8
+
9
9
  validates_presence_of :event_code
10
- validates_presence_of :psp_reference
10
+ validates_presence_of :psp_reference
11
11
  validates_uniqueness_of :success, :scope => [:psp_reference, :event_code]
12
-
12
+
13
13
  # Make sure we don't end up with an original_reference with an empty string
14
14
  before_validation { |notification| notification.original_reference = nil if notification.original_reference.blank? }
15
15
 
@@ -17,37 +17,37 @@ module Adyen
17
17
  converted_params = {}
18
18
  # Convert each attribute from CamelCase notation to under_score notation
19
19
  # For example, merchantReference will be converted to merchant_reference
20
- params.each do |key, value|
20
+ params.each do |key, value|
21
21
  field_name = key.to_s.underscore
22
22
  converted_params[field_name] = value if self.column_names.include?(field_name)
23
23
  end
24
24
  self.create!(converted_params)
25
25
  end
26
-
26
+
27
27
  def authorisation?
28
28
  event_code == 'AUTHORISATION'
29
29
  end
30
-
30
+
31
31
  alias :authorization? :authorisation?
32
-
32
+
33
33
  def successful_authorisation?
34
34
  event_code == 'AUTHORISATION' && success?
35
35
  end
36
-
36
+
37
37
  def collect_payment_for_recurring_contract!(options)
38
38
  # Make sure we convert the value to cents
39
39
  options[:value] = Adyen::Formatter::Price.in_cents(options[:value])
40
40
  raise "This is not a recurring contract!" unless event_code == 'RECURRING_CONTRACT'
41
41
  Adyen::SOAP::RecurringService.submit(options.merge(:recurring_reference => self.psp_reference))
42
42
  end
43
-
43
+
44
44
  def deactivate_recurring_contract!(options)
45
- raise "This is not a recurring contract!" unless event_code == 'RECURRING_CONTRACT'
45
+ raise "This is not a recurring contract!" unless event_code == 'RECURRING_CONTRACT'
46
46
  Adyen::SOAP::RecurringService.deactivate(options.merge(:recurring_reference => self.psp_reference))
47
47
  end
48
-
48
+
49
49
  alias :successful_authorization? :successful_authorisation?
50
-
50
+
51
51
  class HttpPost < Notification
52
52
 
53
53
  def self.log(request)
@@ -55,22 +55,22 @@ module Adyen
55
55
  end
56
56
 
57
57
  def live=(value)
58
- self.write_attribute(:live, [true, 1, '1', 'true'].include?(value))
58
+ self.write_attribute(:live, [true, 1, '1', 'true'].include?(value))
59
59
  end
60
60
 
61
61
  def success=(value)
62
- self.write_attribute(:success, [true, 1, '1', 'true'].include?(value))
63
- end
64
-
62
+ self.write_attribute(:success, [true, 1, '1', 'true'].include?(value))
63
+ end
64
+
65
65
  def value=(value)
66
66
  self.write_attribute(:value, Adyen::Formatter::Price.from_cents(value)) unless value.blank?
67
67
  end
68
68
  end
69
-
69
+
70
70
  class Migration < ActiveRecord::Migration
71
-
71
+
72
72
  def self.up(table_name = Adyen::Notification::DEFAULT_TABLE_NAME)
73
- create_table(table_name) do |t|
73
+ create_table(table_name) do |t|
74
74
  t.boolean :live, :null => false, :default => false
75
75
  t.string :event_code, :null => false
76
76
  t.string :psp_reference, :null => false
@@ -87,15 +87,13 @@ module Adyen
87
87
  t.boolean :processed, :null => false, :default => false
88
88
  t.timestamps
89
89
  end
90
- add_index table_name, [:psp_reference, :event_code, :success], :unique => true
90
+ add_index table_name, [:psp_reference, :event_code, :success], :unique => true, :name => 'adyen_notification_uniqueness'
91
91
  end
92
-
92
+
93
93
  def self.down(table_name = Adyen::Notification::DEFAULT_TABLE_NAME)
94
- remove_index(table_name, [:psp_reference, :event_code, :success])
94
+ remove_index(table_name, :name => 'adyen_notification_uniqueness')
95
95
  drop_table(table_name)
96
96
  end
97
-
98
97
  end
99
-
100
98
  end
101
99
  end