adyen_jpiqueras 2.3.0

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 (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