adyen_jpiqueras 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.travis.yml +30 -0
  4. data/CHANGELOG.md +128 -0
  5. data/CONTRIBUTING.md +85 -0
  6. data/Gemfile +11 -0
  7. data/LICENSE +21 -0
  8. data/README.md +31 -0
  9. data/Rakefile +54 -0
  10. data/adyen_jpiqueras.gemspec +44 -0
  11. data/config.ru +5 -0
  12. data/lib/adyen.rb +16 -0
  13. data/lib/adyen/api.rb +424 -0
  14. data/lib/adyen/api/cacert.pem +3894 -0
  15. data/lib/adyen/api/payment_service.rb +374 -0
  16. data/lib/adyen/api/recurring_service.rb +188 -0
  17. data/lib/adyen/api/response.rb +61 -0
  18. data/lib/adyen/api/simple_soap_client.rb +134 -0
  19. data/lib/adyen/api/templates/payment_service.rb +159 -0
  20. data/lib/adyen/api/templates/recurring_service.rb +71 -0
  21. data/lib/adyen/api/test_helpers.rb +133 -0
  22. data/lib/adyen/api/xml_querier.rb +137 -0
  23. data/lib/adyen/base.rb +17 -0
  24. data/lib/adyen/configuration.rb +179 -0
  25. data/lib/adyen/form.rb +419 -0
  26. data/lib/adyen/hpp.rb +27 -0
  27. data/lib/adyen/hpp/request.rb +192 -0
  28. data/lib/adyen/hpp/response.rb +52 -0
  29. data/lib/adyen/hpp/signature.rb +34 -0
  30. data/lib/adyen/matchers.rb +92 -0
  31. data/lib/adyen/notification_generator.rb +30 -0
  32. data/lib/adyen/railtie.rb +13 -0
  33. data/lib/adyen/rest.rb +67 -0
  34. data/lib/adyen/rest/authorise_payment.rb +234 -0
  35. data/lib/adyen/rest/authorise_recurring_payment.rb +46 -0
  36. data/lib/adyen/rest/client.rb +127 -0
  37. data/lib/adyen/rest/errors.rb +33 -0
  38. data/lib/adyen/rest/modify_payment.rb +89 -0
  39. data/lib/adyen/rest/payout.rb +89 -0
  40. data/lib/adyen/rest/request.rb +104 -0
  41. data/lib/adyen/rest/response.rb +80 -0
  42. data/lib/adyen/rest/signature.rb +27 -0
  43. data/lib/adyen/signature.rb +76 -0
  44. data/lib/adyen/templates/notification_migration.rb +29 -0
  45. data/lib/adyen/templates/notification_model.rb +69 -0
  46. data/lib/adyen/util.rb +147 -0
  47. data/lib/adyen/version.rb +5 -0
  48. data/spec/api/api_spec.rb +231 -0
  49. data/spec/api/payment_service_spec.rb +505 -0
  50. data/spec/api/recurring_service_spec.rb +236 -0
  51. data/spec/api/response_spec.rb +59 -0
  52. data/spec/api/simple_soap_client_spec.rb +133 -0
  53. data/spec/api/spec_helper.rb +463 -0
  54. data/spec/api/test_helpers_spec.rb +84 -0
  55. data/spec/functional/api_spec.rb +117 -0
  56. data/spec/functional/initializer.rb.ci +3 -0
  57. data/spec/functional/initializer.rb.sample +3 -0
  58. data/spec/spec_helper.rb +8 -0
  59. data/test/form_test.rb +303 -0
  60. data/test/functional/payment_authorisation_api_test.rb +107 -0
  61. data/test/functional/payment_modification_api_test.rb +58 -0
  62. data/test/functional/payout_api_test.rb +93 -0
  63. data/test/helpers/capybara.rb +12 -0
  64. data/test/helpers/configure_adyen.rb +6 -0
  65. data/test/helpers/example_server.rb +136 -0
  66. data/test/helpers/public/adyen.encrypt.js +679 -0
  67. data/test/helpers/public/adyen.encrypt.min.js +14 -0
  68. data/test/helpers/test_cards.rb +20 -0
  69. data/test/helpers/views/authorized.erb +7 -0
  70. data/test/helpers/views/hpp.erb +20 -0
  71. data/test/helpers/views/index.erb +6 -0
  72. data/test/helpers/views/pay.erb +36 -0
  73. data/test/helpers/views/redirect_shopper.erb +18 -0
  74. data/test/hpp/signature_test.rb +37 -0
  75. data/test/hpp_test.rb +250 -0
  76. data/test/integration/hpp_integration_test.rb +52 -0
  77. data/test/integration/payment_using_3d_secure_integration_test.rb +41 -0
  78. data/test/integration/payment_with_client_side_encryption_integration_test.rb +26 -0
  79. data/test/rest/signature_test.rb +36 -0
  80. data/test/rest_list_recurring_details_response_test.rb +22 -0
  81. data/test/rest_request_test.rb +43 -0
  82. data/test/rest_response_test.rb +19 -0
  83. data/test/signature_test.rb +76 -0
  84. data/test/test_helper.rb +45 -0
  85. data/test/util_test.rb +78 -0
  86. data/yard_extensions.rb +16 -0
  87. metadata +308 -0
@@ -0,0 +1,33 @@
1
+ module Adyen
2
+ module REST
3
+
4
+ # The main exception class for error reporting when using the REST API Client.
5
+ class Error < Adyen::Error
6
+ end
7
+
8
+ # Exception class for errors on requests
9
+ class RequestValidationFailed < Adyen::REST::Error
10
+ end
11
+
12
+ # Exception class for error responses from the Adyen API.
13
+ #
14
+ # @!attribute category
15
+ # @return [String, nil]
16
+ # @!attribute code
17
+ # @return [Integer, nil]
18
+ # @!attribute description
19
+ # @return [String, nil]
20
+ class ResponseError < Adyen::REST::Error
21
+ attr_accessor :category, :code, :description
22
+
23
+ def initialize(response_body)
24
+ if match = /\A(\w+)\s(\d+)\s(.*)\z/.match(response_body)
25
+ @category, @code, @description = match[1], match[2].to_i, match[3]
26
+ super("API request error: #{description} (code: #{code}/#{category})")
27
+ else
28
+ super("API request error: #{response_body}")
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,89 @@
1
+ module Adyen
2
+ module REST
3
+
4
+ # This module implements the <b>Payment.capture</b> API to capture
5
+ # previously authorised payments.
6
+ module ModifyPayment
7
+ class Request < Adyen::REST::Request
8
+ def set_modification_amount(currency, value)
9
+ self['modification_amount'] = { currency: currency, value: value }
10
+ end
11
+
12
+ alias_method :set_amount, :set_modification_amount
13
+ end
14
+
15
+ class Response < Adyen::REST::Response
16
+ attr_reader :expected_response
17
+
18
+ def initialize(http_response, options = {})
19
+ super
20
+ @expected_response = options[:expects]
21
+ end
22
+
23
+ def received?
24
+ self[:response] == expected_response
25
+ end
26
+ end
27
+
28
+ # Constructs and issues a Payment.capture API call.
29
+ def capture_payment(attributes = {})
30
+ request = capture_payment_request(attributes)
31
+ execute_request(request)
32
+ end
33
+
34
+ def capture_payment_request(attributes = {})
35
+ Adyen::REST::ModifyPayment::Request.new('Payment.capture', attributes,
36
+ response_class: Adyen::REST::ModifyPayment::Response,
37
+ response_options: {
38
+ expects: '[capture-received]'
39
+ }
40
+ )
41
+ end
42
+
43
+ # Constructs and issues a Payment.cancel API call.
44
+ def cancel_payment(attributes = {})
45
+ request = cancel_payment_request(attributes)
46
+ execute_request(request)
47
+ end
48
+
49
+ def cancel_payment_request(attributes = {})
50
+ Adyen::REST::ModifyPayment::Request.new('Payment.cancel', attributes,
51
+ response_class: Adyen::REST::ModifyPayment::Response,
52
+ response_options: {
53
+ expects: '[cancel-received]'
54
+ }
55
+ )
56
+ end
57
+
58
+ # Constructs and issues a Payment.cancel API call.
59
+ def refund_payment(attributes = {})
60
+ request = refund_payment_request(attributes)
61
+ execute_request(request)
62
+ end
63
+
64
+ def refund_payment_request(attributes = {})
65
+ Adyen::REST::ModifyPayment::Request.new('Payment.refund', attributes,
66
+ response_class: Adyen::REST::ModifyPayment::Response,
67
+ response_options: {
68
+ expects: '[refund-received]'
69
+ }
70
+ )
71
+ end
72
+
73
+ # Constructs and issues a Payment.cancel API call.
74
+ def cancel_or_refund_payment(attributes = {})
75
+ request = cancel_or_refund_payment_request(attributes)
76
+ execute_request(request)
77
+ end
78
+
79
+ def cancel_or_refund_payment_request(attributes = {})
80
+ Adyen::REST::ModifyPayment::Request.new('Payment.cancelOrRefund', attributes,
81
+ response_class: Adyen::REST::ModifyPayment::Response,
82
+ response_options: {
83
+ expects: '[cancelOrRefund-received]'
84
+ }
85
+ )
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,89 @@
1
+ module Adyen
2
+ module REST
3
+ # This module implements the <b>Payout</b>
4
+ # API calls, and includes a custom response class to make handling the response easier.
5
+ # https://docs.adyen.com/developers/payout-manual
6
+ module Payout
7
+ class Request < Adyen::REST::Request
8
+ end
9
+
10
+ class Response < Adyen::REST::Response
11
+
12
+ def success?
13
+ result_code == SUCCESS
14
+ end
15
+
16
+ def received?
17
+ result_code == RECEIVED
18
+ end
19
+
20
+ def confirmed?
21
+ response == CONFIRMED
22
+ end
23
+
24
+ def declined?
25
+ response == DECLINED
26
+ end
27
+
28
+ def result_code
29
+ self[:result_code]
30
+ end
31
+
32
+ def psp_reference
33
+ self[:psp_reference]
34
+ end
35
+
36
+ def response
37
+ self[:response]
38
+ end
39
+
40
+ SUCCESS = 'Success'.freeze
41
+ RECEIVED = '[payout-submit-received]'.freeze
42
+ CONFIRMED = '[payout-confirm-received]'.freeze
43
+ DECLINED = '[payout-decline-received]'.freeze
44
+ private_constant :SUCCESS, :RECEIVED, :CONFIRMED, :DECLINED
45
+ end
46
+
47
+ # Constructs and issues a Payment.capture API call.
48
+ def store_payout(attributes = {})
49
+ request = store_request('Payout.storeDetail', attributes)
50
+ execute_request(request)
51
+ end
52
+
53
+ def submit_payout(attributes = {})
54
+ request = store_request('Payout.submit', attributes)
55
+ execute_request(request)
56
+ end
57
+
58
+ def submit_and_store_payout(attributes = {})
59
+ request = store_request('Payout.storeDetailAndSubmit', attributes)
60
+ execute_request(request)
61
+ end
62
+
63
+ def confirm_payout(attributes = {})
64
+ request = review_request('Payout.confirm', attributes)
65
+ execute_request(request)
66
+ end
67
+
68
+ def decline_payout(attributes = {})
69
+ request = review_request('Payout.decline', attributes)
70
+ execute_request(request)
71
+ end
72
+
73
+ private
74
+ # Require you to use a client initialize with payout_store
75
+ def store_request(action, attributes)
76
+ Adyen::REST::Payout::Request.new(action, attributes,
77
+ response_class: Adyen::REST::Payout::Response
78
+ )
79
+ end
80
+
81
+ # Require you to use a client initialize with payout review
82
+ def review_request(action, attributes)
83
+ Adyen::REST::Payout::Request.new(action, attributes,
84
+ response_class: Adyen::REST::Payout::Response
85
+ )
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,104 @@
1
+ require 'adyen/util'
2
+ require 'adyen/rest/errors'
3
+ require 'adyen/rest/response'
4
+
5
+ module Adyen
6
+ module REST
7
+
8
+ # The request object models an API request to be sent to Adyen's webservice.
9
+ #
10
+ # Some API calls may use a subclass to model their request.
11
+ #
12
+ # @!attribute prefix [r]
13
+ # The prefix to use for every request attribute (except action)
14
+ # @return [String]
15
+ # @!attribute form_data [r]
16
+ # The attributes to include in the API request as form data.
17
+ # @return [Hash<String, String>] A dictionary of key value pairs
18
+ # @!required_attributes [r]
19
+ # The list of required attributes that should show up in the request.
20
+ # {#validate!} will fail if any of these attributes is missing or empty.
21
+ # @return [Array<String>]
22
+ # @!attribute response_class [rw]
23
+ # The response class to use to wrap the HTTP response to this request.
24
+ # @return [Class]
25
+ # @!attribute response_options [rw]
26
+ # The options to send to the response class initializer.
27
+ # @return [Hash]
28
+ #
29
+ # @see Adyen::REST::Client
30
+ # @see Adyen::REST::Response
31
+ class Request
32
+ attr_reader :prefix, :form_data, :required_attributes, :path
33
+ attr_accessor :response_class, :response_options
34
+
35
+ def initialize(action, attributes, options = {})
36
+ @form_data = generate_form_data(attributes)
37
+ @path = generate_path(action)
38
+
39
+ @response_class = options[:response_class] || Adyen::REST::Response
40
+ @response_options = options[:response_options] || {}
41
+
42
+ @required_attributes = []
43
+ end
44
+
45
+ # Returns the request's action
46
+ # @return [String]
47
+ def action
48
+ form_data['action']
49
+ end
50
+
51
+ # Retrieves an attribute from the request
52
+ def [](attribute)
53
+ form_data[canonical_name(attribute)]
54
+ end
55
+
56
+ # Sets an attribute on the request
57
+ def []=(attribute, value)
58
+ form_data.merge!(Adyen::Util.flatten(attribute => value))
59
+ value
60
+ end
61
+
62
+ def merchant_account=(value)
63
+ self[:merchantAccount] = value
64
+ end
65
+
66
+ # Runs validations on the request before it is sent.
67
+ # @return [void]
68
+ # @raises [Adyen::REST::RequestValidationFailed]
69
+ def validate!
70
+ required_attributes.each do |attribute|
71
+ if form_data[attribute].nil? || form_data[attribute].empty?
72
+ raise Adyen::REST::RequestValidationFailed, "#{attribute} is empty, but required!"
73
+ end
74
+ end
75
+ end
76
+
77
+ # Builds a Adyen::REST::Response instnace for a given Net::HTTP response.
78
+ # @param http_response [Net::HTTPResponse] The HTTP response return for this request.
79
+ # @return [Adyen::REST::Response] An instance of {Adyen::REST::Response}, or a subclass.
80
+ def build_response(http_response)
81
+ response_class.new(http_response, response_options)
82
+ end
83
+
84
+ protected
85
+
86
+ def canonical_name(name)
87
+ Adyen::Util.camelize(name)
88
+ end
89
+
90
+ # @return [Hash<String, String>] A dictionary of API request attributes that
91
+ def generate_form_data(attributes)
92
+ Adyen::Util.flatten(attributes)
93
+ end
94
+
95
+ def generate_path(action)
96
+ PATH % action.split('.')
97
+ end
98
+
99
+ # @see Adyen::REST::Request#set_path
100
+ PATH = '/pal/servlet/%s/v12/%s'
101
+ private_constant :PATH
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,80 @@
1
+ require 'adyen/util'
2
+
3
+ module Adyen
4
+ module REST
5
+
6
+ # The Response class models the HTTP response that is the result of a
7
+ # API call to Adyen's REST webservice.
8
+ #
9
+ # Some API calls may respond with an instance of a subclass, to make
10
+ # dealing with the response easier.
11
+ #
12
+ # @!attribute http_response [r]
13
+ # The underlying net/http response.
14
+ # @return [Net::HTTPResponse]
15
+ # @!attribute prefix [r]
16
+ # The prefix to use when reading attributes from the response
17
+ # @return [String]
18
+ #
19
+ # @see Adyen::REST::Client
20
+ # @see Adyen::REST::Request
21
+ class Response
22
+ attr_reader :http_response, :prefix, :attributes
23
+
24
+ def initialize(http_response, options = {})
25
+ @http_response = http_response
26
+ @prefix = options.key?(:prefix) ? options[:prefix].to_s : nil
27
+ @attributes = parse_response_attributes
28
+ end
29
+
30
+ # Looks up an attribute in the response.
31
+ # @return [String, nil] The value of the attribute if it was included in the response.
32
+ def [](name)
33
+ attributes[canonical_name(name)]
34
+ end
35
+
36
+ def has_attribute?(name)
37
+ attributes.has_key?(canonical_name(name))
38
+ end
39
+
40
+ def psp_reference
41
+ Integer(self[:psp_reference])
42
+ end
43
+
44
+ protected
45
+
46
+ def map_response_list(response_prefix, mapped_attributes)
47
+ list = []
48
+ index = 0
49
+
50
+ loop do
51
+ response = {}
52
+ mapped_attributes.each do |key, value|
53
+ new_value = attributes["#{response_prefix}.#{index.to_s}.#{value}"]
54
+ response[key] = new_value unless new_value.empty?
55
+ end
56
+
57
+ index += 1
58
+ break unless response.any?
59
+ list << response
60
+ end
61
+
62
+ list
63
+ end
64
+
65
+ def canonical_name(name)
66
+ Adyen::Util.camelize(apply_prefix(name))
67
+ end
68
+
69
+ def apply_prefix(name)
70
+ prefix ? name.to_s.sub(/\A(?!#{Regexp.quote(prefix)}\.)/, "#{prefix}.") : name.to_s
71
+ end
72
+
73
+ def parse_response_attributes
74
+ attributes = CGI.parse(http_response.body)
75
+ attributes.each { |key, values| attributes[key] = values.first }
76
+ attributes
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,27 @@
1
+ require 'adyen/signature'
2
+
3
+ module Adyen
4
+ module REST
5
+ # The Signature module can sign and verify HMAC SHA-256 signatures for API
6
+ module Signature
7
+ extend self
8
+
9
+ # Sign the parameters with the given shared secret
10
+ # @param [Hash] params The set of parameters to sign. Should sent `sharedSecret` to sign.
11
+ # @return [String] signature from parameters
12
+ def sign(params)
13
+ Adyen::Signature.sign(params, :rest)
14
+ end
15
+
16
+ # Verify the parameters with the given shared secret
17
+ # @param [Hash] params The set of parameters to verify.
18
+ # Should include `sharedSecret` param to sign and the `hmacSignature` param to compare with the signature calculated
19
+ # @return [Boolean] true if the `hmacSignature` in the params matches our calculated signature
20
+ def verify(params)
21
+ their_sig = params.delete('hmacSignature')
22
+ raise ArgumentError, "params must include 'hmacSignature' for verification" if their_sig.empty?
23
+ Adyen::Signature.verify(params, their_sig, :rest)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,76 @@
1
+ require 'openssl'
2
+ require 'base64'
3
+
4
+ module Adyen
5
+ # The Signature module generic to sign and verify HMAC SHA-256 signatures
6
+ module Signature
7
+ extend self
8
+
9
+ # Sign the parameters with the given shared secret
10
+ # @param [Hash] params The set of parameters to verify. Must include a `shared_secret` param for signing/verification
11
+ #
12
+ # @param [String] type The type to sign (:hpp or :rest). Default is :hpp
13
+ # @return [String] The signature
14
+ def sign(params, type = :hpp)
15
+ shared_secret = params.delete('sharedSecret')
16
+ raise ArgumentError, "Cannot sign parameters without a shared secret" unless shared_secret
17
+ sig = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), Array(shared_secret).pack("H*"), string_to_sign(params, type))
18
+ Base64.encode64(sig).strip
19
+ end
20
+
21
+ # Compare a signature calculated with anoter HMAC Signature
22
+ # @param [Hash] params The set of parameters to verify. Must include a `shared_secret`
23
+ # param for signing/verification
24
+ # @param [String] hmacSignature will be compared to the signature calculated.
25
+ # @return [Boolean] true if the `hmacSignature` matches our calculated signature
26
+ def verify(params, hmacSignature, type = :hpp)
27
+ raise ArgumentError,"hmacSignature cannot be empty for verification" if hmacSignature.empty?
28
+ our_sig = sign(params, type)
29
+ secure_compare(hmacSignature, our_sig)
30
+ end
31
+
32
+ private
33
+
34
+ def string_to_sign(params, type)
35
+ string = ''
36
+ if type == :hpp
37
+ string = sorted_keys(params) + sorted_values(params)
38
+ elsif type == :rest
39
+ keys = %w(pspReference originalReference merchantAccountCode merchantReference value currency eventCode success)
40
+ string = sorted_values(params, keys)
41
+ else
42
+ raise NotImplementedError, 'Type sign not implemented'
43
+ end
44
+
45
+ string.map{ |el| escape_value(el) }.join(':')
46
+ end
47
+
48
+ def sorted_keys(hash, keys_to_sort = nil)
49
+ hash.sort.map{ |el| el[0] }
50
+ end
51
+
52
+ def sorted_values(hash, keys_to_sort = nil)
53
+ if keys_to_sort.is_a? Array
54
+ keys_to_sort.map { |key| hash[key] }
55
+ else
56
+ hash.sort.map{ |el| el[1] }
57
+ end
58
+ end
59
+
60
+ def escape_value(value)
61
+ value.gsub(':', '\\:').gsub('\\', '\\\\')
62
+ end
63
+
64
+ # Constant-time compare for two fixed-length strings
65
+ # Stolen from https://github.com/rails/rails/commit/c8c660002f4b0e9606de96325f20b95248b6ff2d
66
+ def secure_compare(a, b)
67
+ return false unless a.bytesize == b.bytesize
68
+
69
+ l = a.unpack "C#{a.bytesize}"
70
+
71
+ res = 0
72
+ b.each_byte { |byte| res |= byte ^ l.shift }
73
+ res == 0
74
+ end
75
+ end
76
+ end