adyen 0.3.8 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/.kick +35 -0
- data/LICENSE +3 -2
- data/README.rdoc +8 -4
- data/Rakefile +10 -0
- data/TODO +14 -4
- data/adyen.gemspec +9 -15
- data/lib/adyen.rb +10 -59
- data/lib/adyen/api.rb +281 -0
- data/lib/adyen/api/cacert.pem +3509 -0
- data/lib/adyen/api/payment_service.rb +258 -0
- data/lib/adyen/api/recurring_service.rb +126 -0
- data/lib/adyen/api/response.rb +54 -0
- data/lib/adyen/api/simple_soap_client.rb +118 -0
- data/lib/adyen/api/templates/payment_service.rb +103 -0
- data/lib/adyen/api/templates/recurring_service.rb +34 -0
- data/lib/adyen/api/test_helpers.rb +133 -0
- data/lib/adyen/api/xml_querier.rb +94 -0
- data/lib/adyen/configuration.rb +139 -0
- data/lib/adyen/form.rb +37 -109
- data/lib/adyen/formatter.rb +0 -10
- data/lib/adyen/matchers.rb +1 -1
- data/lib/adyen/notification_generator.rb +30 -0
- data/lib/adyen/railtie.rb +13 -0
- data/lib/adyen/templates/notification_migration.rb +29 -0
- data/lib/adyen/templates/notification_model.rb +70 -0
- data/spec/adyen_spec.rb +3 -45
- data/spec/api/api_spec.rb +139 -0
- data/spec/api/payment_service_spec.rb +439 -0
- data/spec/api/recurring_service_spec.rb +105 -0
- data/spec/api/response_spec.rb +35 -0
- data/spec/api/simple_soap_client_spec.rb +91 -0
- data/spec/api/spec_helper.rb +417 -0
- data/spec/api/test_helpers_spec.rb +83 -0
- data/spec/form_spec.rb +27 -23
- data/spec/functional/api_spec.rb +90 -0
- data/spec/functional/initializer.rb.sample +3 -0
- data/spec/spec_helper.rb +5 -5
- data/tasks/github-gem.rake +49 -55
- data/yard_extensions.rb +16 -0
- metadata +63 -82
- data/init.rb +0 -1
- data/lib/adyen/notification.rb +0 -151
- data/lib/adyen/soap.rb +0 -649
- data/spec/notification_spec.rb +0 -97
- 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
|