adyen 0.3.8 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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