adyen 0.3.8 → 1.0.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 (46) hide show
  1. data/.gitignore +4 -0
  2. data/.kick +35 -0
  3. data/LICENSE +3 -2
  4. data/README.rdoc +8 -4
  5. data/Rakefile +10 -0
  6. data/TODO +14 -4
  7. data/adyen.gemspec +9 -15
  8. data/lib/adyen.rb +10 -59
  9. data/lib/adyen/api.rb +281 -0
  10. data/lib/adyen/api/cacert.pem +3509 -0
  11. data/lib/adyen/api/payment_service.rb +258 -0
  12. data/lib/adyen/api/recurring_service.rb +126 -0
  13. data/lib/adyen/api/response.rb +54 -0
  14. data/lib/adyen/api/simple_soap_client.rb +118 -0
  15. data/lib/adyen/api/templates/payment_service.rb +103 -0
  16. data/lib/adyen/api/templates/recurring_service.rb +34 -0
  17. data/lib/adyen/api/test_helpers.rb +133 -0
  18. data/lib/adyen/api/xml_querier.rb +94 -0
  19. data/lib/adyen/configuration.rb +139 -0
  20. data/lib/adyen/form.rb +37 -109
  21. data/lib/adyen/formatter.rb +0 -10
  22. data/lib/adyen/matchers.rb +1 -1
  23. data/lib/adyen/notification_generator.rb +30 -0
  24. data/lib/adyen/railtie.rb +13 -0
  25. data/lib/adyen/templates/notification_migration.rb +29 -0
  26. data/lib/adyen/templates/notification_model.rb +70 -0
  27. data/spec/adyen_spec.rb +3 -45
  28. data/spec/api/api_spec.rb +139 -0
  29. data/spec/api/payment_service_spec.rb +439 -0
  30. data/spec/api/recurring_service_spec.rb +105 -0
  31. data/spec/api/response_spec.rb +35 -0
  32. data/spec/api/simple_soap_client_spec.rb +91 -0
  33. data/spec/api/spec_helper.rb +417 -0
  34. data/spec/api/test_helpers_spec.rb +83 -0
  35. data/spec/form_spec.rb +27 -23
  36. data/spec/functional/api_spec.rb +90 -0
  37. data/spec/functional/initializer.rb.sample +3 -0
  38. data/spec/spec_helper.rb +5 -5
  39. data/tasks/github-gem.rake +49 -55
  40. data/yard_extensions.rb +16 -0
  41. metadata +63 -82
  42. data/init.rb +0 -1
  43. data/lib/adyen/notification.rb +0 -151
  44. data/lib/adyen/soap.rb +0 -649
  45. data/spec/notification_spec.rb +0 -97
  46. data/spec/soap_spec.rb +0 -340
@@ -0,0 +1,258 @@
1
+ require 'adyen/api/simple_soap_client'
2
+ require 'adyen/api/templates/payment_service'
3
+
4
+ module Adyen
5
+ module API
6
+ # This is the class that maps actions to Adyen’s Payment SOAP service.
7
+ #
8
+ # It’s encouraged to use the shortcut methods on the {API} module, which abstracts away the
9
+ # difference between this service and the {RecurringService}. Henceforth, for extensive
10
+ # documentation you should look at the {API} documentation.
11
+ #
12
+ # The most important difference is that you instantiate a {PaymentService} with the parameters
13
+ # that are needed for the call that you will eventually make.
14
+ #
15
+ # @example
16
+ # payment = Adyen::API::PaymentService.new({
17
+ # :reference => invoice.id,
18
+ # :amount => {
19
+ # :currency => 'EUR',
20
+ # :value => invoice.amount,
21
+ # },
22
+ # :shopper => {
23
+ # :email => user.email,
24
+ # :reference => user.id,
25
+ # #:ip => request.,
26
+ # },
27
+ # :card => {
28
+ # :expiry_month => 12,
29
+ # :expiry_year => 2012,
30
+ # :holder_name => 'Simon Hopper',
31
+ # :number => '4444333322221111',
32
+ # :cvc => '737'
33
+ # }
34
+ # })
35
+ # response = payment.authorise_payment
36
+ # response.authorised? # => true
37
+ #
38
+ class PaymentService < SimpleSOAPClient
39
+ # The Adyen Payment SOAP service endpoint uri.
40
+ ENDPOINT_URI = 'https://pal-%s.adyen.com/pal/servlet/soap/Payment'
41
+
42
+ # @see API.authorise_payment
43
+ def authorise_payment
44
+ make_payment_request(authorise_payment_request_body, AuthorisationResponse)
45
+ end
46
+
47
+ # @see API.authorise_recurring_payment
48
+ def authorise_recurring_payment
49
+ make_payment_request(authorise_recurring_payment_request_body, AuthorisationResponse)
50
+ end
51
+
52
+ # @see API.authorise_one_click_payment
53
+ def authorise_one_click_payment
54
+ make_payment_request(authorise_one_click_payment_request_body, AuthorisationResponse)
55
+ end
56
+
57
+ # @see API.capture_payment
58
+ def capture
59
+ make_payment_request(capture_request_body, CaptureResponse)
60
+ end
61
+
62
+ # @see API.refund_payment
63
+ def refund
64
+ make_payment_request(refund_request_body, RefundResponse)
65
+ end
66
+
67
+ # @see API.cancel_payment
68
+ def cancel
69
+ make_payment_request(cancel_request_body, CancelResponse)
70
+ end
71
+
72
+ # @see API.cancel_or_refund_payment
73
+ def cancel_or_refund
74
+ make_payment_request(cancel_or_refund_request_body, CancelOrRefundResponse)
75
+ end
76
+
77
+ private
78
+
79
+ def make_payment_request(data, response_class)
80
+ call_webservice_action('authorise', data, response_class)
81
+ end
82
+
83
+ def authorise_payment_request_body
84
+ content = card_partial
85
+ if @params[:recurring]
86
+ validate_parameters!(:shopper => [:email, :reference])
87
+ content << ENABLE_RECURRING_CONTRACTS_PARTIAL
88
+ end
89
+ payment_request_body(content)
90
+ end
91
+
92
+ def authorise_recurring_payment_request_body
93
+ validate_parameters!(:shopper => [:email, :reference])
94
+ content = RECURRING_PAYMENT_BODY_PARTIAL % (@params[:recurring_detail_reference] || 'LATEST')
95
+ payment_request_body(content)
96
+ end
97
+
98
+ def authorise_one_click_payment_request_body
99
+ validate_parameters!(:recurring_detail_reference,
100
+ :shopper => [:email, :reference],
101
+ :card => [:cvc])
102
+ content = ONE_CLICK_PAYMENT_BODY_PARTIAL % [@params[:recurring_detail_reference], @params[:card][:cvc]]
103
+ payment_request_body(content)
104
+ end
105
+
106
+ def payment_request_body(content)
107
+ validate_parameters!(:merchant_account, :reference, :amount => [:currency, :value])
108
+ content << amount_partial
109
+ content << shopper_partial if @params[:shopper]
110
+ LAYOUT % [@params[:merchant_account], @params[:reference], content]
111
+ end
112
+
113
+ def capture_request_body
114
+ CAPTURE_LAYOUT % capture_and_refund_params
115
+ end
116
+
117
+ def refund_request_body
118
+ REFUND_LAYOUT % capture_and_refund_params
119
+ end
120
+
121
+ def cancel_or_refund_request_body
122
+ validate_parameters!(:merchant_account, :psp_reference)
123
+ CANCEL_OR_REFUND_LAYOUT % [@params[:merchant_account], @params[:psp_reference]]
124
+ end
125
+
126
+ def cancel_request_body
127
+ validate_parameters!(:merchant_account, :psp_reference)
128
+ CANCEL_LAYOUT % [@params[:merchant_account], @params[:psp_reference]]
129
+ end
130
+
131
+ def capture_and_refund_params
132
+ validate_parameters!(:merchant_account, :psp_reference, :amount => [:currency, :value])
133
+ [@params[:merchant_account], @params[:psp_reference], *@params[:amount].values_at(:currency, :value)]
134
+ end
135
+
136
+ def amount_partial
137
+ AMOUNT_PARTIAL % @params[:amount].values_at(:currency, :value)
138
+ end
139
+
140
+ def card_partial
141
+ validate_parameters!(:card => [:holder_name, :number, :cvc, :expiry_year, :expiry_month])
142
+ card = @params[:card].values_at(:holder_name, :number, :cvc, :expiry_year)
143
+ card << @params[:card][:expiry_month].to_i
144
+ CARD_PARTIAL % card
145
+ end
146
+
147
+ def shopper_partial
148
+ @params[:shopper].map { |k, v| SHOPPER_PARTIALS[k] % v }.join("\n")
149
+ end
150
+
151
+ class AuthorisationResponse < Response
152
+ ERRORS = {
153
+ "validation 101 Invalid card number" => [:number, 'is not a valid creditcard number'],
154
+ "validation 103 CVC is not the right length" => [:cvc, 'is not the right length'],
155
+ "validation 128 Card Holder Missing" => [:holder_name, "can't be blank"],
156
+ "validation Couldn't parse expiry year" => [:expiry_year, 'could not be recognized'],
157
+ "validation Expiry month should be between 1 and 12 inclusive" => [:expiry_month, 'could not be recognized'],
158
+ }
159
+
160
+ AUTHORISED = 'Authorised'
161
+
162
+ def self.original_fault_message_for(attribute, message)
163
+ if error = ERRORS.find { |_, (a, m)| a == attribute && m == message }
164
+ error.first
165
+ else
166
+ message
167
+ end
168
+ end
169
+
170
+ response_attrs :result_code, :auth_code, :refusal_reason, :psp_reference
171
+
172
+ def success?
173
+ super && params[:result_code] == AUTHORISED
174
+ end
175
+
176
+ alias authorized? success?
177
+
178
+ # @return [Boolean] Returns whether or not the request was valid.
179
+ def invalid_request?
180
+ !fault_message.nil?
181
+ end
182
+
183
+ # In the case of a validation error, or SOAP fault message, this method will return an
184
+ # array describing what attribute failed validation and the accompanying message. If the
185
+ # errors is not of the common user validation errors, then the attribute is +:base+ and the
186
+ # full original message is returned.
187
+ #
188
+ # An optional +prefix+ can be given so you can seamlessly integrate this in your
189
+ # ActiveRecord model and copy over errors.
190
+ #
191
+ # @param [String,Symbol] prefix A string that should be used to prefix the error key.
192
+ # @return [Array<Symbol, String>] A name-message pair of the attribute with an error.
193
+ def error(prefix = nil)
194
+ if error = ERRORS[fault_message]
195
+ prefix ? ["#{prefix}_#{error[0]}".to_sym, error[1]] : error
196
+ else
197
+ [:base, fault_message]
198
+ end
199
+ end
200
+
201
+ def params
202
+ @params ||= xml_querier.xpath('//payment:authoriseResponse/payment:paymentResult') do |result|
203
+ {
204
+ :psp_reference => result.text('./payment:pspReference'),
205
+ :result_code => result.text('./payment:resultCode'),
206
+ :auth_code => result.text('./payment:authCode'),
207
+ :refusal_reason => (invalid_request? ? fault_message : result.text('./payment:refusalReason'))
208
+ }
209
+ end
210
+ end
211
+ end
212
+
213
+ class ModificationResponse < Response
214
+ class << self
215
+ # @private
216
+ attr_accessor :request_received_value, :base_xpath
217
+ end
218
+
219
+ response_attrs :psp_reference, :response
220
+
221
+ # This only returns whether or not the request has been successfully received. Check the
222
+ # subsequent notification to see if the payment was actually mutated.
223
+ def success?
224
+ super && params[:response] == self.class.request_received_value
225
+ end
226
+
227
+ def params
228
+ @params ||= xml_querier.xpath(self.class.base_xpath) do |result|
229
+ {
230
+ :psp_reference => result.text('./payment:pspReference'),
231
+ :response => result.text('./payment:response')
232
+ }
233
+ end
234
+ end
235
+ end
236
+
237
+ class CaptureResponse < ModificationResponse
238
+ self.request_received_value = '[capture-received]'
239
+ self.base_xpath = '//payment:captureResponse/payment:captureResult'
240
+ end
241
+
242
+ class RefundResponse < ModificationResponse
243
+ self.request_received_value = '[refund-received]'
244
+ self.base_xpath = '//payment:refundResponse/payment:refundResult'
245
+ end
246
+
247
+ class CancelResponse < ModificationResponse
248
+ self.request_received_value = '[cancel-received]'
249
+ self.base_xpath = '//payment:cancelResponse/payment:cancelResult'
250
+ end
251
+
252
+ class CancelOrRefundResponse < ModificationResponse
253
+ self.request_received_value = '[cancelOrRefund-received]'
254
+ self.base_xpath = '//payment:cancelOrRefundResponse/payment:cancelOrRefundResult'
255
+ end
256
+ end
257
+ end
258
+ end
@@ -0,0 +1,126 @@
1
+ require 'adyen/api/simple_soap_client'
2
+ require 'adyen/api/templates/recurring_service'
3
+
4
+ module Adyen
5
+ module API
6
+ # This is the class that maps actions to Adyen’s Recurring SOAP service.
7
+ #
8
+ # It’s encouraged to use the shortcut methods on the {API} module, which abstracts away the
9
+ # difference between this service and the {PaymentService}. Henceforth, for extensive
10
+ # documentation you should look at the {API} documentation.
11
+ #
12
+ # The most important difference is that you instantiate a {RecurringService} with the parameters
13
+ # that are needed for the call that you will eventually make.
14
+ #
15
+ # @example
16
+ # recurring = Adyen::API::RecurringService.new(:shopper => { :reference => user.id })
17
+ # response = recurring.disable
18
+ # response.success? # => true
19
+ #
20
+ class RecurringService < SimpleSOAPClient
21
+ # The Adyen Recurring SOAP service endpoint uri.
22
+ ENDPOINT_URI = 'https://pal-%s.adyen.com/pal/servlet/soap/Recurring'
23
+
24
+ # @see API.list_recurring_details
25
+ def list
26
+ call_webservice_action('listRecurringDetails', list_request_body, ListResponse)
27
+ end
28
+
29
+ # @see API.disable_recurring_contract
30
+ def disable
31
+ call_webservice_action('disable', disable_request_body, DisableResponse)
32
+ end
33
+
34
+ private
35
+
36
+ def list_request_body
37
+ validate_parameters!(:merchant_account, :shopper => [:reference])
38
+ LIST_LAYOUT % [@params[:merchant_account], @params[:shopper][:reference]]
39
+ end
40
+
41
+ def disable_request_body
42
+ validate_parameters!(:merchant_account, :shopper => [:reference])
43
+ if reference = @params[:recurring_detail_reference]
44
+ reference = RECURRING_DETAIL_PARTIAL % reference
45
+ end
46
+ DISABLE_LAYOUT % [@params[:merchant_account], @params[:shopper][:reference], reference || '']
47
+ end
48
+
49
+ class DisableResponse < Response
50
+ DISABLED_RESPONSES = %w{ [detail-successfully-disabled] [all-details-successfully-disabled] }
51
+
52
+ response_attrs :response
53
+
54
+ def success?
55
+ super && DISABLED_RESPONSES.include?(params[:response])
56
+ end
57
+
58
+ alias disabled? success?
59
+
60
+ def params
61
+ @params ||= { :response => xml_querier.text('//recurring:disableResponse/recurring:result/recurring:response') }
62
+ end
63
+ end
64
+
65
+ class ListResponse < Response
66
+ response_attrs :details, :last_known_shopper_email, :shopper_reference, :creation_date
67
+
68
+ def references
69
+ details.map { |d| d[:recurring_detail_reference] }
70
+ end
71
+
72
+ def params
73
+ @params ||= xml_querier.xpath('//recurring:listRecurringDetailsResponse/recurring:result') do |result|
74
+ details = result.xpath('.//recurring:RecurringDetail')
75
+ details.empty? ? {} : {
76
+ :creation_date => DateTime.parse(result.text('./recurring:creationDate')),
77
+ :details => details.map { |node| parse_recurring_detail(node) },
78
+ :last_known_shopper_email => result.text('./recurring:lastKnownShopperEmail'),
79
+ :shopper_reference => result.text('./recurring:shopperReference')
80
+ }
81
+ end
82
+ end
83
+
84
+ private
85
+
86
+ # @todo add support for elv
87
+ def parse_recurring_detail(node)
88
+ result = {
89
+ :recurring_detail_reference => node.text('./recurring:recurringDetailReference'),
90
+ :variant => node.text('./recurring:variant'),
91
+ :creation_date => DateTime.parse(node.text('./recurring:creationDate'))
92
+ }
93
+
94
+ card = node.xpath('./recurring:card')
95
+ if card.children.empty?
96
+ result[:bank] = parse_bank_details(node.xpath('./recurring:bank'))
97
+ else
98
+ result[:card] = parse_card_details(card)
99
+ end
100
+
101
+ result
102
+ end
103
+
104
+ def parse_card_details(card)
105
+ {
106
+ :expiry_date => Date.new(card.text('./payment:expiryYear').to_i, card.text('./payment:expiryMonth').to_i, -1),
107
+ :holder_name => card.text('./payment:holderName'),
108
+ :number => card.text('./payment:number')
109
+ }
110
+ end
111
+
112
+ def parse_bank_details(bank)
113
+ {
114
+ :bank_account_number => bank.text('./payment:bankAccountNumber'),
115
+ :bank_location_id => bank.text('./payment:bankLocationId'),
116
+ :bank_name => bank.text('./payment:bankName'),
117
+ :bic => bank.text('./payment:bic'),
118
+ :country_code => bank.text('./payment:countryCode'),
119
+ :iban => bank.text('./payment:iban'),
120
+ :owner_name => bank.text('./payment:ownerName')
121
+ }
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,54 @@
1
+ module Adyen
2
+ module API
3
+ # The base class of all responses returned by API calls to Adyen.
4
+ class Response
5
+ # Defines shortcut accessor methods, to {Response#params}, for the given parameters.
6
+ def self.response_attrs(*attrs)
7
+ attrs.each do |attr|
8
+ define_method(attr) { params[attr] }
9
+ end
10
+ end
11
+
12
+ # @return [Net::HTTPResponse] The response object returned by Net::HTTP.
13
+ attr_reader :http_response
14
+
15
+ # @param [Net::HTTPResponse] http_response The response object returned by Net::HTTP.
16
+ def initialize(http_response)
17
+ @http_response = http_response
18
+ end
19
+
20
+ # @return [String] The raw body of the response object.
21
+ def body
22
+ @http_response.body
23
+ end
24
+
25
+ # @return [Boolean] Whether or not the request was successful.
26
+ def success?
27
+ !http_failure?
28
+ end
29
+
30
+ # @return [Boolean] Whether or not the HTTP request was a success.
31
+ def http_failure?
32
+ !@http_response.is_a?(Net::HTTPSuccess)
33
+ end
34
+
35
+ # @return [XMLQuerier] The response body wrapped in a XMLQuerier.
36
+ def xml_querier
37
+ @xml_querier ||= XMLQuerier.new(@http_response.body)
38
+ end
39
+
40
+ # @return [Hash] Subclasses return the parsed response body.
41
+ def params
42
+ raise "The Adyen::API::Response#params method should be overridden in a subclass."
43
+ end
44
+
45
+ # @return [String,nil] The SOAP failure message, if there is one.
46
+ def fault_message
47
+ @fault_message ||= begin
48
+ message = xml_querier.text('//soap:Fault/faultstring')
49
+ message unless message.empty?
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,118 @@
1
+ require 'net/https'
2
+
3
+ require 'adyen/api/response'
4
+ require 'adyen/api/xml_querier'
5
+
6
+ module Adyen
7
+ module API
8
+ # The base class of the API classes that map to Adyen SOAP services.
9
+ class SimpleSOAPClient
10
+ # @private
11
+ ENVELOPE = <<EOS
12
+ <?xml version="1.0"?>
13
+ <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
14
+ <soap:Body>
15
+ %s
16
+ </soap:Body>
17
+ </soap:Envelope>
18
+ EOS
19
+
20
+ # A CA file used to verify certificates when connecting to Adyen.
21
+ #
22
+ # @see http://curl.haxx.se/ca/cacert.pem
23
+ CACERT = File.expand_path('../cacert.pem', __FILE__)
24
+
25
+ class ClientError < StandardError
26
+ def initialize(response, action, endpoint)
27
+ @response, @action, @endpoint = response, action, endpoint
28
+ end
29
+
30
+ def message
31
+ "[#{@response.code} #{@response.message}] A client error occurred while calling SOAP action `#{@action}' on endpoint `#{@endpoint}'."
32
+ end
33
+ end
34
+
35
+ class << self
36
+ # When a response instance has been assigned, the subsequent call to
37
+ # {SimpleSOAPClient#call_webservice_action} will not make a remote call, but simply return
38
+ # the stubbed response instance. This is obviously meant for making payments from tests.
39
+ #
40
+ # @see PaymentService::TestHelpers
41
+ # @see RecurringService::TestHelpers
42
+ #
43
+ # @return [Response] The stubbed Response subclass instance.
44
+ attr_accessor :stubbed_response
45
+
46
+ # @return [URI] A URI based on the ENDPOINT_URI constant defined on subclasses, where
47
+ # the environment type has been interpolated. E.g. Test environment.
48
+ def endpoint
49
+ @endpoint ||= URI.parse(const_get('ENDPOINT_URI') % Adyen.configuration.environment)
50
+ end
51
+ end
52
+
53
+ # @return [Hash] A hash of key-value pairs required for the action that is to be called.
54
+ attr_reader :params
55
+
56
+ # @param [Hash] params A hash of key-value pairs required for the action that is to be called.
57
+ # These are merged with the Adyen::API.default_params.
58
+ def initialize(params = {})
59
+ @params = Adyen.configuration.default_api_params.merge(params)
60
+ end
61
+
62
+ def validate_parameter_value!(param, value)
63
+ if value.nil? || value =~ /^\s*$/
64
+ raise ArgumentError, "The required parameter `:#{param}' is missing."
65
+ end
66
+ end
67
+
68
+ def validate_parameters!(*params)
69
+ params.each do |param|
70
+ case param
71
+ when Symbol
72
+ validate_parameter_value!(param, @params[param])
73
+ when Hash
74
+ param.each do |name, attrs|
75
+ validate_parameter_value!(name, @params[name])
76
+ attrs.each { |attr| validate_parameter_value!("#{name} => :#{attr}", @params[name][attr]) }
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ # This method wraps the given XML +data+ in a SOAP envelope and posts it to +action+ on the
83
+ # +endpoint+ defined for the subclass.
84
+ #
85
+ # The result is a response object, with XMLQuerier, ready to be queried.
86
+ #
87
+ # If a {stubbed_response} has been set, then said response is returned and no actual remote
88
+ # calls are made.
89
+ #
90
+ # @param [String] action The remote action to call.
91
+ # @param [String] data The XML data to post to the remote action.
92
+ # @param [Response] response_class The Response subclass used to wrap the response from Adyen.
93
+ def call_webservice_action(action, data, response_class)
94
+ if response = self.class.stubbed_response
95
+ self.class.stubbed_response = nil
96
+ response
97
+ else
98
+ endpoint = self.class.endpoint
99
+
100
+ post = Net::HTTP::Post.new(endpoint.path, 'Accept' => 'text/xml', 'Content-Type' => 'text/xml; charset=utf-8', 'SOAPAction' => action)
101
+ post.basic_auth(Adyen.configuration.api_username, Adyen.configuration.api_password)
102
+ post.body = ENVELOPE % data
103
+
104
+ request = Net::HTTP.new(endpoint.host, endpoint.port)
105
+ request.use_ssl = true
106
+ request.ca_file = CACERT
107
+ request.verify_mode = OpenSSL::SSL::VERIFY_PEER
108
+
109
+ request.start do |http|
110
+ http_response = http.request(post)
111
+ raise ClientError.new(http_response, action, endpoint) if http_response.is_a?(Net::HTTPClientError)
112
+ response_class.new(http_response)
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end