adyen 1.6.0 → 2.0.0.pre1

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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -0
  3. data/CHANGELOG.md +116 -0
  4. data/CONTRIBUTING.md +84 -0
  5. data/Gemfile +1 -1
  6. data/README.md +33 -0
  7. data/Rakefile +19 -4
  8. data/adyen.gemspec +7 -5
  9. data/config.ru +5 -0
  10. data/lib/adyen.rb +2 -2
  11. data/lib/adyen/configuration.rb +6 -0
  12. data/lib/adyen/form.rb +13 -48
  13. data/lib/adyen/matchers.rb +1 -1
  14. data/lib/adyen/rest.rb +53 -0
  15. data/lib/adyen/rest/authorise_payment.rb +148 -0
  16. data/lib/adyen/rest/client.rb +110 -0
  17. data/lib/adyen/rest/errors.rb +33 -0
  18. data/lib/adyen/rest/modify_payment.rb +97 -0
  19. data/lib/adyen/rest/request.rb +107 -0
  20. data/lib/adyen/rest/response.rb +59 -0
  21. data/lib/adyen/util.rb +147 -0
  22. data/lib/adyen/version.rb +1 -1
  23. data/test/form_test.rb +17 -10
  24. data/test/functional/payment_authorisation_api_test.rb +54 -0
  25. data/test/functional/payment_modification_api_test.rb +57 -0
  26. data/test/helpers/configure_adyen.rb +5 -0
  27. data/test/helpers/example_server.rb +105 -0
  28. data/test/helpers/public/adyen.encrypt.js +679 -0
  29. data/test/helpers/public/adyen.encrypt.min.js +14 -0
  30. data/test/helpers/test_cards.rb +20 -0
  31. data/test/helpers/views/authorized.erb +7 -0
  32. data/test/helpers/views/hpp.erb +19 -0
  33. data/test/helpers/views/index.erb +6 -0
  34. data/test/helpers/views/pay.erb +36 -0
  35. data/test/helpers/views/redirect_shopper.erb +18 -0
  36. data/test/integration/hpp_integration_test.rb +52 -0
  37. data/test/integration/payment_using_3d_secure_integration_test.rb +40 -0
  38. data/test/integration/payment_with_client_side_encryption_integration_test.rb +26 -0
  39. data/test/rest_request_test.rb +50 -0
  40. data/test/rest_response_test.rb +18 -0
  41. data/test/test_helper.rb +32 -0
  42. data/test/util_test.rb +77 -0
  43. metadata +89 -16
  44. data/README.rdoc +0 -50
  45. data/lib/adyen/encoding.rb +0 -21
  46. data/lib/adyen/formatter.rb +0 -33
  47. data/test/adyen_test.rb +0 -31
@@ -0,0 +1,107 @@
1
+ module Adyen
2
+ module REST
3
+
4
+ # The request object models an API request to be sent to Adyen's webservice.
5
+ #
6
+ # Some API calls may use a subclass to model their request.
7
+ #
8
+ # @!attribute prefix [r]
9
+ # The prefix to use for every request attribute (except action)
10
+ # @return [String]
11
+ # @!attribute form_data [r]
12
+ # The attributes to include in the API request as form data.
13
+ # @return [Hash<String, String>] A dictionary of key value pairs
14
+ # @!required_attributes [r]
15
+ # The list of required attributes that should show up in the request.
16
+ # {#validate!} will fail if any of these attributes is missing or empty.
17
+ # @return [Array<String>]
18
+ # @!attribute response_class [rw]
19
+ # The response class to use to wrap the HTTP response to this request.
20
+ # @return [Class]
21
+ # @!attribute response_options [rw]
22
+ # The options to send to the response class initializer.
23
+ # @return [Hash]
24
+ #
25
+ # @see Adyen::REST::Client
26
+ # @see Adyen::REST::Response
27
+ class Request
28
+ attr_reader :prefix, :form_data, :required_attributes
29
+ attr_accessor :response_class, :response_options
30
+
31
+ def initialize(action, attributes, options = {})
32
+ @prefix = options[:prefix]
33
+ @form_data = generate_form_data(action, attributes)
34
+
35
+ @response_class = options[:response_class] || Adyen::REST::Response
36
+ @response_options = options[:response_options] || {}
37
+
38
+ @required_attributes = ['action']
39
+ end
40
+
41
+ # Returns the request's action
42
+ # @return [String]
43
+ def action
44
+ form_data['action']
45
+ end
46
+
47
+ # Retrieves an attribute from the request
48
+ def [](attribute)
49
+ form_data[canonical_name(attribute)]
50
+ end
51
+
52
+ # Sets an attribute on the request
53
+ def []=(attribute, value)
54
+ form_data.merge!(flatten_attributes(attribute => value))
55
+ value
56
+ end
57
+
58
+ def merchant_account=(value)
59
+ self[:merchant_account] = value
60
+ end
61
+
62
+ # Runs validations on the request before it is sent.
63
+ # @return [void]
64
+ # @raises [Adyen::REST::RequestValidationFailed]
65
+ def validate!
66
+ required_attributes.each do |attribute|
67
+ if form_data[attribute].nil? || form_data[attribute].empty?
68
+ raise Adyen::REST::RequestValidationFailed, "#{attribute} is empty, but required!"
69
+ end
70
+ end
71
+ end
72
+
73
+ # Builds a Adyen::REST::Response instnace for a given Net::HTTP response.
74
+ # @param http_response [Net::HTTPResponse] The HTTP response return for this request.
75
+ # @return [Adyen::REST::Response] An instance of {Adyen::REST::Response}, or a subclass.
76
+ def build_response(http_response)
77
+ response_class.new(http_response, response_options)
78
+ end
79
+
80
+ protected
81
+
82
+ def canonical_name(name)
83
+ Adyen::Util.camelize(apply_prefix(name))
84
+ end
85
+
86
+ def apply_prefix(name)
87
+ prefix ? name.to_s.sub(/\A(?!#{Regexp.quote(prefix)}\.)/, "#{prefix}.") : name.to_s
88
+ end
89
+
90
+ # Flattens the {#attributes} hash and converts all the keys to camelcase.
91
+ # @return [Hash] A potentially nested hash of attributes.
92
+ # @return [Hash<String, String>] A dictionary of API request attributes that
93
+ # can be included in an HTTP request as form data.
94
+ def flatten_attributes(attributes)
95
+ if prefix
96
+ Adyen::Util.flatten(prefix => attributes)
97
+ else
98
+ Adyen::Util.flatten(attributes)
99
+ end
100
+ end
101
+
102
+ def generate_form_data(action, attributes)
103
+ flatten_attributes(attributes).merge('action' => action.to_s)
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,59 @@
1
+ module Adyen
2
+ module REST
3
+
4
+ # The Response class models the HTTP response that is the result of a
5
+ # API call to Adyen's REST webservice.
6
+ #
7
+ # Some API calls may respond with an instance of a subclass, to make
8
+ # dealing with the response easier.
9
+ #
10
+ # @!attribute http_response [r]
11
+ # The underlying net/http response.
12
+ # @return [Net::HTTPResponse]
13
+ # @!attribute prefix [r]
14
+ # The prefix to use when reading attributes from the response
15
+ # @return [String]
16
+ #
17
+ # @see Adyen::REST::Client
18
+ # @see Adyen::REST::Request
19
+ class Response
20
+ attr_reader :http_response, :prefix, :attributes
21
+
22
+ def initialize(http_response, options = {})
23
+ @http_response = http_response
24
+ @prefix = options.key?(:prefix) ? options[:prefix].to_s : nil
25
+ @attributes = parse_response_attributes
26
+ end
27
+
28
+ # Looks up an attribute in the response.
29
+ # @return [String, nil] The value of the attribute if it was included in the response.
30
+ def [](name)
31
+ attributes[canonical_name(name)]
32
+ end
33
+
34
+ def has_attribute?(name)
35
+ attributes.has_key?(canonical_name(name))
36
+ end
37
+
38
+ def psp_reference
39
+ Integer(self[:psp_reference])
40
+ end
41
+
42
+ protected
43
+
44
+ def canonical_name(name)
45
+ Adyen::Util.camelize(apply_prefix(name))
46
+ end
47
+
48
+ def apply_prefix(name)
49
+ prefix ? name.to_s.sub(/\A(?!#{Regexp.quote(prefix)}\.)/, "#{prefix}.") : name.to_s
50
+ end
51
+
52
+ def parse_response_attributes
53
+ attributes = CGI.parse(http_response.body)
54
+ attributes.each { |key, values| attributes[key] = values.first }
55
+ attributes
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,147 @@
1
+ require 'date'
2
+ require 'openssl'
3
+ require 'base64'
4
+
5
+ module Adyen
6
+ module Util
7
+ extend self
8
+
9
+ # Returns a valid Adyen string representation for a date
10
+ def format_date(date)
11
+ case date
12
+ when Date, DateTime, Time
13
+ date.strftime('%Y-%m-%d')
14
+ when String
15
+ raise ArgumentError, "Invalid date notation: #{date.inspect}!" unless /^\d{4}-\d{2}-\d{2}$/ =~ date
16
+ date
17
+ else
18
+ raise ArgumentError, "Cannot convert #{date.inspect} to date!"
19
+ end
20
+ end
21
+
22
+ # Returns a valid Adyen string representation for a timestamp
23
+ def format_timestamp(time)
24
+ case time
25
+ when Date, DateTime, Time
26
+ time.strftime('%Y-%m-%dT%H:%M:%SZ')
27
+ when String
28
+ raise ArgumentError, "Invalid timestamp notation: #{time.inspect}!" unless /^\d{4}-\d{2}-\d{2}T\d{2}\:\d{2}\:\d{2}Z$/ =~ time
29
+ time
30
+ else
31
+ raise ArgumentError, "Cannot convert #{time.inspect} to timestamp!"
32
+ end
33
+ end
34
+
35
+ # Returns a base64-encoded signature for a message
36
+ # @param [String] hmac_key The secret key to use for the HMAC signature.
37
+ # @param [String] message The message to sign.
38
+ # @return [String] The signature, base64-encoded.
39
+ def hmac_base64(hmac_key, message)
40
+ digest = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha1'), hmac_key, message)
41
+ Base64.strict_encode64(digest).strip
42
+ end
43
+
44
+ # Retuns a message gzip-compressed and base64-encoded.
45
+ # @param [String] message The message to compress and encode.
46
+ # @return [String] The compressed and encoded version of the message
47
+ def gzip_base64(message)
48
+ sio = StringIO.new
49
+ gz = Zlib::GzipWriter.new(sio)
50
+ gz.write(message)
51
+ gz.close
52
+ Base64.strict_encode64(sio.string)
53
+ end
54
+
55
+ # Returns the camelized version of a string.
56
+ # @param [:to_s] identifier The identifier to turn to camelcase.
57
+ # @return [String] The camelcase version of the identifier provided.
58
+ def camelize(identifier)
59
+ CAMELCASE_EXCEPTIONS[identifier.to_s] || identifier.to_s.gsub(/_+(.)/) { $1.upcase }
60
+ end
61
+
62
+ # Returns the underscore version of a string.
63
+ # @param [:to_s] identifier The identifier to turn to underscore notation.
64
+ # @return [String] The underscore version of the identifier provided.
65
+ def underscore(identifier)
66
+ UNDERSCORE_EXCEPTIONS[identifier.to_s] || identifier.to_s
67
+ .gsub(/([A-Z]{2,})([A-Z])/) { "#{$1.downcase}#{$2}" }
68
+ .gsub(/(?!\A)([A-Z][a-z]*)/, '_\1')
69
+ .downcase
70
+ end
71
+
72
+ # Transforms the nested parameters Hash into a 'flat' Hash which is understood by adyen. This is:
73
+ # * all keys are camelized
74
+ # * all keys are stringified
75
+ # * nested hash is flattened, keys are prefixed with root key
76
+ #
77
+ # @example
78
+ # flatten {:billing_address => { :street => 'My Street'}}
79
+ #
80
+ # # resolves in:
81
+ # {'billingAddress.street' => 'My Street'}
82
+ #
83
+ # @param [Hash] nested_hash The nested hash to transform
84
+ # @param [String] prefix The prefix to add to the key
85
+ # @param [Hash] return_hash The new hash which is retruned (needed for recursive calls)
86
+ # @return [Hash] The return hash filled with camelized and prefixed key, stringified value
87
+ def flatten(nested_hash, prefix = "", return_hash = {})
88
+ nested_hash ||= {}
89
+ nested_hash.inject(return_hash) do |hash, (key, value)|
90
+ key = "#{prefix}#{camelize(key)}"
91
+ if value.is_a?(Hash)
92
+ flatten(value, "#{key}.", return_hash)
93
+ else
94
+ hash[key] = value.to_s
95
+ end
96
+ hash
97
+ end
98
+ end
99
+
100
+ # Transforms a flat hash into a nested hash structure.
101
+ # * all keys are underscored
102
+ # * all keys are stringified
103
+ # * flattened hash is deflattened, using . as namespace separator
104
+ #
105
+ # @example
106
+ # deflatten {'billingAddress.street' => 'My Street'}
107
+ #
108
+ # # resolves in:
109
+ # {'billing_address' => { 'street' => 'My Street'}}
110
+ #
111
+ # @param [Hash] flattened_hash The flat hash to transform
112
+ # @param [Hash] return_hash The new hash which will be returned (needed for recursive calls)
113
+ # @return [Hash] A nested hash structure, using strings as key.
114
+ def deflatten(flattened_hash, return_hash = {})
115
+ return return_hash if flattened_hash.nil?
116
+ flattened_hash.each do |key, value|
117
+ deflatten_pair(key, value, return_hash)
118
+ end
119
+ return_hash
120
+ end
121
+
122
+ private
123
+
124
+ def deflatten_pair(key, value, return_hash)
125
+ head, rest = key.split('.', 2)
126
+ key = underscore(head)
127
+ if rest.nil?
128
+ raise ArgumentError, "Duplicate key in flattened hash." if return_hash.key?(key)
129
+ return_hash[key] = value
130
+ else
131
+ return_hash[key] ||= {}
132
+ raise ArgumentError, "Key nesting conflict in flattened hash." unless return_hash[key].is_a?(Hash)
133
+ deflatten_pair(rest, value, return_hash[key])
134
+ end
135
+ end
136
+
137
+ # This hash contains exceptions to the standard underscore to camelcase conversion rules.
138
+ CAMELCASE_EXCEPTIONS = {
139
+ 'shopper_ip' => 'shopperIP'
140
+ }
141
+
142
+ # This hash contains exceptions to the standard camelcase to underscore conversion rules.
143
+ UNDERSCORE_EXCEPTIONS = CAMELCASE_EXCEPTIONS.invert
144
+
145
+ private_constant :CAMELCASE_EXCEPTIONS, :UNDERSCORE_EXCEPTIONS
146
+ end
147
+ end
@@ -1,5 +1,5 @@
1
1
  module Adyen
2
2
  # Version constant for the Adyen plugin.
3
3
  # Set it & commit the change before running rake release.
4
- VERSION = "1.6.0"
4
+ VERSION = "2.0.0.pre1"
5
5
  end
@@ -29,7 +29,7 @@ class FormTest < Minitest::Test
29
29
  :state_or_province => 'Berlin',
30
30
  :country => 'Germany',
31
31
  },
32
- :shopper => {
32
+ :shopper => {
33
33
  :telephone_number => '1234512345',
34
34
  :first_name => 'John',
35
35
  :last_name => 'Doe',
@@ -90,7 +90,7 @@ class FormTest < Minitest::Test
90
90
 
91
91
  params = CGI.parse(redirect_uri.query)
92
92
  attributes.each do |key, value|
93
- assert_equal value.to_s, params[Adyen::Form.camelize(key).to_s].first
93
+ assert_equal value.to_s, params[Adyen::Util.camelize(key).to_s].first
94
94
  end
95
95
 
96
96
  assert params.key?('merchantSig'), "Expected a merchantSig parameter to be set"
@@ -107,7 +107,7 @@ class FormTest < Minitest::Test
107
107
 
108
108
  params = CGI.parse(redirect_uri.query)
109
109
  attributes.each do |key, value|
110
- assert_equal value.to_s, params[Adyen::Form.camelize(key).to_s].first
110
+ assert_equal value.to_s, params[Adyen::Util.camelize(key).to_s].first
111
111
  end
112
112
 
113
113
  assert params.key?('merchantSig'), "Expected a merchantSig parameter to be set"
@@ -158,9 +158,9 @@ class FormTest < Minitest::Test
158
158
 
159
159
  def test_redirect_signature_check
160
160
  params = {
161
- :authResult => 'AUTHORISED', :pspReference => '1211992213193029',
162
- :merchantReference => 'Internet Order 12345', :skinCode => '4aD37dJA',
163
- :merchantSig => 'ytt3QxWoEhAskUzUne0P5VA9lPw='
161
+ 'authResult' => 'AUTHORISED', 'pspReference' => '1211992213193029',
162
+ 'merchantReference' => 'Internet Order 12345', 'skinCode' => '4aD37dJA',
163
+ 'merchantSig' => 'ytt3QxWoEhAskUzUne0P5VA9lPw='
164
164
  }
165
165
 
166
166
  assert_equal params[:merchantSig], Adyen::Form.redirect_signature(params)
@@ -179,10 +179,17 @@ class FormTest < Minitest::Test
179
179
  assert_raises(ArgumentError) { Adyen::Form.redirect_signature_check(params.delete(:skinCode)) }
180
180
  end
181
181
 
182
- def test_flatten
183
- parameters = { 'billingAddress.street' => 'My Street' }
184
- assert_equal parameters, Adyen::Form.flatten(:billing_address => { :street => 'My Street'})
185
- assert_equal Hash.new, Adyen::Form.flatten(nil)
182
+ def test_redirect_signature_check
183
+ Adyen.configuration.register_form_skin(:testing, 'tifSfXeX', 'testing123', :merchant_account => 'VanBergenORG')
184
+
185
+ # http://example.com/result?merchantReference=HPP+test+order+%25231&skinCode=tifSfXeX&shopperLocale=en_GB&paymentMethod=visa&authResult=AUTHORISED&pspReference=8814131153369759&merchantSig=il8cjgOiG4N9l2PlSf6h4EVQ6hk%253D
186
+ params = {
187
+ "merchantReference"=>CGI.unescape("HPP test order %231"), "skinCode"=>"tifSfXeX",
188
+ "shopperLocale"=>"en_GB", "paymentMethod"=>"visa", "authResult"=>"AUTHORISED",
189
+ "pspReference"=>"8814131148758652", "merchantSig"=> CGI.unescape("q8J9P%2Fp%2FYsbnnFn%2F83TFsv7Hais%3D")
190
+ }
191
+
192
+ assert_equal params['merchantSig'], Adyen::Form.redirect_signature(params)
186
193
  end
187
194
 
188
195
  def test_hidden_payment_form_fields
@@ -0,0 +1,54 @@
1
+ require 'test_helper'
2
+
3
+ class PaymentAuthorisationAPITest < Minitest::Test
4
+ def setup
5
+ @client = Adyen::REST.client
6
+ end
7
+
8
+ def teardown
9
+ @client.close
10
+ end
11
+
12
+ def test_payment_api_request
13
+ response = @client.authorise_payment(
14
+ merchant_account: 'VanBergenORG',
15
+ amount: { currency: 'EUR', value: 1234 },
16
+ reference: 'Test order #1',
17
+ card: Adyen::TestCards::VISA
18
+ )
19
+
20
+ assert response.authorised?
21
+ assert response.psp_reference
22
+ end
23
+
24
+ def test_refused_payment_api_request
25
+ response = @client.authorise_payment(
26
+ merchant_account: 'VanBergenORG',
27
+ amount: { currency: 'EUR', value: 1234 },
28
+ reference: 'Test order #1',
29
+ card: Adyen::TestCards::VISA.merge(cvc: '123')
30
+ )
31
+
32
+ assert response.refused?
33
+ assert response.psp_reference
34
+ assert response.has_attribute?(:refusal_reason)
35
+ end
36
+
37
+ def test_payment_with_3d_secure_api_request
38
+ response = @client.authorise_payment(
39
+ merchant_account: 'VanBergenORG',
40
+ amount: { currency: 'EUR', value: 1234 },
41
+ reference: 'Test order #1',
42
+ card: Adyen::TestCards::MASTERCARD_3DSECURE,
43
+ browser_info: {
44
+ acceptHeader: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
45
+ userAgent: "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9) Gecko/2008052912 Firefox/3.0"
46
+ }
47
+ )
48
+
49
+ assert response.redirect_shopper?
50
+ assert response.psp_reference
51
+ assert response.has_attribute?(:md)
52
+ assert_equal "https://test.adyen.com/hpp/3d/validate.shtml", response['issuer_url']
53
+ end
54
+ end
@@ -0,0 +1,57 @@
1
+ require 'test_helper'
2
+
3
+ class PaymentModificationAPITest < Minitest::Test
4
+ def setup
5
+ @client = Adyen::REST.client
6
+ end
7
+
8
+ def teardown
9
+ @client.close
10
+ end
11
+
12
+ def test_capture_payment_api_request
13
+ response = @client.capture_payment(
14
+ merchant_account: 'VanBergenORG',
15
+ modification_amount: { currency: 'EUR', value: 1234 },
16
+ reference: "functional test for cancellation",
17
+ original_reference: 7913939284323855
18
+ )
19
+
20
+ assert response.received?
21
+ assert response.psp_reference
22
+ end
23
+
24
+ def test_cancel_payment_api_request
25
+ response = @client.cancel_payment(
26
+ merchant_account: 'VanBergenORG',
27
+ reference: "functional test for cancellation",
28
+ original_reference: 7913939284323855
29
+ )
30
+
31
+ assert response.received?
32
+ assert response.psp_reference
33
+ end
34
+
35
+ def test_refund_payment_api_request
36
+ response = @client.refund_payment(
37
+ merchant_account: 'VanBergenORG',
38
+ modification_amount: { currency: 'EUR', value: 1234 },
39
+ reference: "functional test for cancellation",
40
+ original_reference: 7913939284323855
41
+ )
42
+
43
+ assert response.received?
44
+ assert response.psp_reference
45
+ end
46
+
47
+ def test_cancel_or_refund_payment_api_request
48
+ response = @client.cancel_or_refund_payment(
49
+ merchant_account: 'VanBergenORG',
50
+ reference: "functional test for cancellation",
51
+ original_reference: 7913939284323855
52
+ )
53
+
54
+ assert response.received?
55
+ assert response.psp_reference
56
+ end
57
+ end