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
data/lib/adyen/hpp.rb ADDED
@@ -0,0 +1,27 @@
1
+ require 'adyen/hpp/signature'
2
+ require 'adyen/hpp/request'
3
+ require 'adyen/hpp/response'
4
+
5
+ module Adyen
6
+ module HPP
7
+
8
+ # The DOMAIN of the Adyen payment system that still requires the current
9
+ # Adyen enviroment.
10
+ HPP_DOMAIN = "%s.adyen.com"
11
+
12
+ # The URL of the Adyen payment system that still requires the current
13
+ # domain and payment flow to be filled.
14
+ HPP_URL = "https://%s/hpp/%s.shtml"
15
+
16
+ class Error < Adyen::Error
17
+ end
18
+
19
+ class ForgedResponse < Adyen::HPP::Error
20
+ end
21
+
22
+ class Notification
23
+ def initialize(request)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,192 @@
1
+ require 'adyen/hpp/signature'
2
+ require 'cgi'
3
+
4
+ module Adyen
5
+ module HPP
6
+
7
+ class Request
8
+ attr_accessor :parameters
9
+ attr_writer :skin, :environment, :shared_secret
10
+
11
+ # Initialize the HPP request
12
+ #
13
+ # @param [Hash] parameters The payment parameters
14
+ # You must not provide the +:merchant_sig+ parameter: it will be calculated automatically.
15
+ # @param [Hash|String] skin A skin hash in the same format that is returned by
16
+ # Adyen::Configuration.register_form_skin, or the name of a registered skin.
17
+ # When not set, the default skin specified in the configuration will be used.
18
+ # @param [String] environment The Adyen environment to use.
19
+ # When not set, the environment specified in the configuration will be used.
20
+ # @param [String] shared_secret The shared secret to use for signing the request.
21
+ # When not set, the shared secret of the skin will be used.
22
+ def initialize(parameters, skin: nil, environment: nil, shared_secret: nil)
23
+ @parameters, @skin, @environment, @shared_secret = parameters, skin, environment, shared_secret
24
+ @skin = Adyen.configuration.form_skin_by_name(@skin) unless skin.nil? || skin.is_a?(Hash)
25
+ end
26
+
27
+ # Returns the Adyen skin to use for the request, in the same format that is
28
+ # returned by Adyen::Configuration.register_form_skin
29
+ #
30
+ # @return [Hash] skin if set, configuration default otherwise
31
+ def skin
32
+ @skin || Adyen.configuration.form_skin_by_name(Adyen.configuration.default_skin) || {}
33
+ end
34
+
35
+ # Returns the Adyen environment the request will be directed to
36
+ #
37
+ # @return [String] environment if set, configuration default otherwise
38
+ def environment
39
+ @environment || Adyen.configuration.environment
40
+ end
41
+
42
+ # Returns the shared secret to use for signing the request
43
+ #
44
+ # @return [String] shared secret if set, the skin's shared secret otherwise
45
+ def shared_secret
46
+ @shared_secret || skin[:shared_secret]
47
+ end
48
+
49
+ # Returns the DOMAIN of the Adyen payment system, adjusted for an Adyen environment.
50
+ #
51
+ # @return [String] The domain of the Adyen payment system that can be used
52
+ # for payment forms or redirects.
53
+ # @see Adyen::HPP::Request.redirect_url
54
+ def domain
55
+ (Adyen.configuration.payment_flow_domain || HPP_DOMAIN) % [environment.to_s]
56
+ end
57
+
58
+ # Returns the URL of the Adyen payment system, adjusted for an Adyen environment.
59
+ #
60
+ # @param [String] payment_flow The Adyen payment type to use. This parameter can be
61
+ # left out, in which case the default payment type will be used.
62
+ # @return [String] The absolute URL of the Adyen payment system that can be used
63
+ # for payment forms or redirects.
64
+ # @see Adyen::HPP::Request.domain
65
+ # @see Adyen::HPP::Request.redirect_url
66
+ # @see Adyen::HPP::Request.payment_methods_url
67
+ def url(payment_flow = nil)
68
+ payment_flow ||= Adyen.configuration.payment_flow
69
+ HPP_URL % [domain, payment_flow.to_s]
70
+ end
71
+
72
+ # Transforms the payment parameters hash to be in the correct format. It will also
73
+ # include the Adyen::Configuration#default_form_params hash and it will
74
+ # include the +:skin_code+ parameter and the default attributes of the skin
75
+ # Any default parameter value will be overrided if another value is provided in the request.
76
+ #
77
+ # @return [Hash] Completed and formatted payment parameters.
78
+ # @raise [ArgumentError] Thrown if some parameter health check fails.
79
+ def formatted_parameters
80
+ raise ArgumentError, "Cannot generate request: parameters should be a hash!" unless parameters.is_a?(Hash)
81
+
82
+ formatted_parameters = parameters
83
+ default_form_parameters = Adyen.configuration.default_form_params
84
+ unless skin.empty?
85
+ formatted_parameters[:skin_code] ||= skin[:skin_code]
86
+ default_form_parameters = default_form_parameters.merge(skin[:default_form_params] || {})
87
+ end
88
+ formatted_parameters = default_form_parameters.merge(formatted_parameters)
89
+
90
+ raise ArgumentError, "Cannot generate request: :currency code attribute not found!" unless formatted_parameters[:currency_code]
91
+ raise ArgumentError, "Cannot generate request: :payment_amount code attribute not found!" unless formatted_parameters[:payment_amount]
92
+ raise ArgumentError, "Cannot generate request: :merchant_account attribute not found!" unless formatted_parameters[:merchant_account]
93
+ raise ArgumentError, "Cannot generate request: :skin_code attribute not found!" unless formatted_parameters[:skin_code]
94
+
95
+ formatted_parameters[:recurring_contract] = 'RECURRING' if formatted_parameters.delete(:recurring) == true
96
+ formatted_parameters[:order_data] = Adyen::Util.gzip_base64(formatted_parameters.delete(:order_data_raw)) if formatted_parameters[:order_data_raw]
97
+ formatted_parameters[:ship_before_date] = Adyen::Util.format_date(formatted_parameters[:ship_before_date])
98
+ formatted_parameters[:session_validity] = Adyen::Util.format_timestamp(formatted_parameters[:session_validity])
99
+ formatted_parameters
100
+ end
101
+
102
+ # Transforms and flattens payment parameters to be in the correct format which is understood and accepted by adyen
103
+ #
104
+ # @return [Hash] The payment parameters, with camelized and prefixed key, stringified values and
105
+ # the +:merchant_signature+ parameter set.
106
+ def flat_payment_parameters
107
+ Adyen::HPP::Signature.sign(Adyen::Util.flatten(formatted_parameters), shared_secret)
108
+ end
109
+
110
+ # Returns an absolute URL to the Adyen payment system, with the payment parameters included
111
+ # as GET parameters in the URL. The URL also depends on the Adyen enviroment
112
+ #
113
+ # Note that Internet Explorer has a maximum length for URLs it can handle (2083 characters).
114
+ # Make sure that the URL is not longer than this limit if you want your site to work in IE.
115
+ #
116
+ # @example
117
+ #
118
+ # def pay
119
+ # # Generate a URL to redirect to Adyen's payment system.
120
+ # payment_parameters = {
121
+ # :currency_code => 'USD',
122
+ # :payment_amount => 1000,
123
+ # :merchant_account => 'MyMerchant',
124
+ # ...
125
+ # }
126
+ # hpp_request = Adyen::HPP::Request.new(payment_parameters, skin: :my_skin, environment: :test)
127
+ #
128
+ # respond_to do |format|
129
+ # format.html { redirect_to(hpp_request.redirect_url) }
130
+ # end
131
+ # end
132
+ #
133
+ # @return [String] An absolute URL to redirect to the Adyen payment system.
134
+ def redirect_url
135
+ url + '?' + flat_payment_parameters.map { |(k, v)|
136
+ "#{CGI.escape(k)}=#{CGI.escape(v)}"
137
+ }.join('&')
138
+ end
139
+
140
+ # @see Adyen::HPP::Request.redirect_url
141
+ #
142
+ # Returns an absolute URL very similar to the one returned by Adyen::HPP::Request.redirect_url
143
+ # except that it uses the directory.shtml call which returns a list of all available
144
+ # payment methods
145
+ #
146
+ # @return [String] An absolute URL to redirect to the Adyen payment system.
147
+ def payment_methods_url
148
+ url(:directory) + '?' + flat_payment_parameters.map { |(k, v)|
149
+ "#{CGI.escape(k)}=#{CGI.escape(v)}"
150
+ }.join('&')
151
+ end
152
+
153
+ # Returns a HTML snippet of hidden INPUT tags with the provided payment parameters.
154
+ # The snippet can be included in a payment form that POSTs to the Adyen payment system.
155
+ #
156
+ # The payment parameters that are provided to this method will be merged with the
157
+ # {Adyen::Configuration#default_form_params} hash. The default parameter values will be
158
+ # overrided if another value is provided to this method.
159
+ #
160
+ # You do not have to provide the +:merchant_sig+ parameter: it will be calculated automatically.
161
+ #
162
+ # @example
163
+ #
164
+ # <%
165
+ # payment_parameters = {
166
+ # :currency_code => 'USD',
167
+ # :payment_amount => 1000,
168
+ # :merchant_account => 'MyMerchant',
169
+ # ...
170
+ # }
171
+ # hpp_request = Adyen::HPP::Request.new(payment_parameters, skin: :my_skin, environment: :test)
172
+ # %>
173
+ #
174
+ # <%= form_tag(hpp_request.url, authenticity_token: false, enforce_utf8: false) do %>
175
+ # <%= hpp_request.hidden_fields %>
176
+ # <%= submit_tag("Pay invoice")
177
+ # <% end %>
178
+ #
179
+ # @return [String] An HTML snippet that can be included in a form that POSTs to the
180
+ # Adyen payment system.
181
+ def hidden_fields
182
+
183
+ # Generate a hidden input tag per parameter, join them by newlines.
184
+ form_str = flat_payment_parameters.map { |key, value|
185
+ "<input type=\"hidden\" name=\"#{CGI.escapeHTML(key)}\" value=\"#{CGI.escapeHTML(value)}\" />"
186
+ }.join("\n")
187
+
188
+ form_str.respond_to?(:html_safe) ? form_str.html_safe : form_str
189
+ end
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,52 @@
1
+ module Adyen
2
+ module HPP
3
+
4
+ class Response
5
+ attr_reader :params, :shared_secret
6
+
7
+ # Initialize the HPP response
8
+ #
9
+ # @param [Hash] params params A hash of HTTP GET parameters for the redirect request. This
10
+ # should include the +:merchantSig+ parameter, which contains the signature.
11
+ # @param [String] shared_secret Optional shared secret; if not provided, the shared secret
12
+ # of the skin determined by params['skinCode'] will be used
13
+ def initialize(params, shared_secret: nil)
14
+ raise ArgumentError, "params should be a Hash" unless params.is_a?(Hash)
15
+ raise ArgumentError, "params should contain :merchantSig" unless params.key?('merchantSig')
16
+
17
+ @params = params
18
+ skin = Adyen.configuration.form_skin_by_code(params['skinCode']) || {}
19
+ @shared_secret = shared_secret || skin[:shared_secret]
20
+ end
21
+
22
+ # Checks the redirect signature for this request by calculating the signature from
23
+ # the provided parameters, and comparing it to the signature provided in the +merchantSig+
24
+ # parameter.
25
+ #
26
+ # If this method returns false, the request could be a forgery and should not be handled.
27
+ # Therefore, you should include this check in a +before_filter+, and raise an error of the
28
+ # signature check fails.
29
+ #
30
+ # @example
31
+ # class PaymentsController < ApplicationController
32
+ # before_filter :check_signature, :only => [:return_from_adyen]
33
+ #
34
+ # def return_from_adyen
35
+ # @invoice = Invoice.find(params[:merchantReference])
36
+ # @invoice.set_paid! if params[:authResult] == 'AUTHORISED'
37
+ # end
38
+ #
39
+ # private
40
+ #
41
+ # def check_signature
42
+ # raise "Forgery!" unless Adyen::HPP::Response.new(params).has_valid_signature?
43
+ # end
44
+ # end
45
+ #
46
+ # @return [true, false] Returns true only if the signature in the parameters is correct.
47
+ def has_valid_signature?
48
+ Adyen::HPP::Signature.verify(params, shared_secret)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,34 @@
1
+ require 'adyen/signature'
2
+
3
+ module Adyen
4
+ module HPP
5
+ # The Signature module can sign and verify HMAC SHA-256 signatures for Hosted Payment Pages
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.
11
+ # @param [String] shared_secret The shared secret for signing.
12
+ # @return [Hash] params The params that were passed in plus a new `merchantSig` param
13
+ def sign(params, shared_secret)
14
+ params = params.dup
15
+ params.delete('merchantSig')
16
+ params["sharedSecret"] ||= shared_secret
17
+ params.merge('merchantSig' => Adyen::Signature.sign(params))
18
+ end
19
+
20
+ # Verify the parameters with the given shared secret
21
+ # @param [Hash] params The set of parameters to verify. Must include a `merchantSig`
22
+ # param that will be compared to the signature we calculate.
23
+ # @param [String] shared_secret The shared secret for verification.
24
+ # @return [Boolean] true if the `merchantSig` in the params matches our calculated signature
25
+ def verify(params, shared_secret)
26
+ params = params.dup
27
+ params["sharedSecret"] ||= shared_secret
28
+ their_sig = params.delete('merchantSig')
29
+ raise ArgumentError, "params must include 'merchantSig' for verification" if their_sig.empty?
30
+ Adyen::Signature.verify(params, their_sig)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,92 @@
1
+ require 'adyen/api/xml_querier'
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[@id='adyen']"
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 = "\n descendant::input[@type='hidden'][@name='#{Adyen::Util.camelize(key)}']"
25
+ condition << "[@value='#{value}']" unless value == :anything
26
+ xpath_query << "[#{condition}]"
27
+ end
28
+
29
+ return xpath_query
30
+ end
31
+
32
+ def self.check(subject, checks = {})
33
+ document = Adyen::API::XMLQuerier.html(subject)
34
+ result = document.xpath(build_xpath_query(checks))
35
+ !result.empty?
36
+ end
37
+ end
38
+
39
+ class HaveAdyenPaymentForm
40
+
41
+ def initialize(checks)
42
+ @checks = checks
43
+ end
44
+
45
+ def matches?(document)
46
+ Adyen::Matchers::XPathPaymentFormCheck.check(document, @checks)
47
+ end
48
+
49
+ def description
50
+ "have an adyen payment form"
51
+ end
52
+
53
+ def failure_message
54
+ "expected to find a valid Adyen form on this page"
55
+ end
56
+
57
+ def negative_failure_message
58
+ "expected not to find a valid Adyen form on this page"
59
+ end
60
+ end
61
+
62
+ def have_adyen_payment_form(checks = {})
63
+ default_checks = {:merchant_sig => :anything, :payment_amount => :anything, :currency_code => :anything, :skin_code => :anything }
64
+ HaveAdyenPaymentForm.new(default_checks.merge(checks))
65
+ end
66
+
67
+ def have_adyen_recurring_payment_form(checks = {})
68
+ recurring_checks = { :recurring => true, :shopper_email => :anything, :shopper_reference => :anything }
69
+ have_adyen_payment_form(recurring_checks.merge(checks))
70
+ end
71
+
72
+ def have_adyen_single_payment_form(checks = {})
73
+ recurring_checks = { :recurring => false }
74
+ have_adyen_payment_form(recurring_checks.merge(checks))
75
+ end
76
+
77
+ def assert_adyen_payment_form(subject, checks = {})
78
+ default_checks = {:merchant_sig => :anything, :payment_amount => :anything, :currency_code => :anything, :skin_code => :anything }
79
+ assert Adyen::Matchers::XPathPaymentFormCheck.check(subject, default_checks.merge(checks)), 'No Adyen payment form found'
80
+ end
81
+
82
+ def assert_adyen_recurring_payment_form(subject, checks = {})
83
+ recurring_checks = { :recurring => true, :shopper_email => :anything, :shopper_reference => :anything }
84
+ assert_adyen_payment_form(subject, recurring_checks.merge(checks))
85
+ end
86
+
87
+ def assert_adyen_single_payment_form(subject, checks = {})
88
+ recurring_checks = { :recurring => false }
89
+ assert_adyen_payment_form(subject, recurring_checks.merge(checks))
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,30 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/migration'
3
+
4
+ # @private
5
+ class Adyen::NotificationGenerator < Rails::Generators::Base
6
+ include Rails::Generators::Migration
7
+
8
+ def self.source_root
9
+ @source_root ||= File.join(File.dirname(__FILE__), 'templates')
10
+ end
11
+
12
+ # Implement the required interface for Rails::Generators::Migration.
13
+ # taken from http://github.com/rails/rails/blob/master/activerecord/lib/generators/active_record.rb
14
+ def self.next_migration_number(dirname)
15
+ if ActiveRecord::Base.timestamped_migrations
16
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
17
+ else
18
+ "%.3d" % (current_migration_number(dirname) + 1)
19
+ end
20
+ end
21
+
22
+ # Create a migration file for the adyen_notifications table
23
+ def create_migration_file
24
+ migration_template 'notification_migration.rb', 'db/migrate/create_adyen_notifications.rb'
25
+ end
26
+
27
+ def create_model_file
28
+ template 'notification_model.rb', 'app/models/adyen_notification.rb'
29
+ end
30
+ end
@@ -0,0 +1,13 @@
1
+ require 'rails'
2
+
3
+ # @private
4
+ class Adyen::Railtie < ::Rails::Railtie
5
+
6
+ generators do
7
+ require 'adyen/notification_generator'
8
+ end
9
+
10
+ config.before_configuration do
11
+ config.adyen = Adyen.configuration
12
+ end
13
+ end