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