This repository has been archived by the owner on Sep 23, 2020. It is now read-only.
/
payment_service.rb
268 lines (227 loc) · 9.38 KB
/
payment_service.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
require 'adyen/api/simple_soap_client'
require 'adyen/api/templates/payment_service'
module Adyen
module API
# This is the class that maps actions to Adyen’s Payment SOAP service.
#
# It’s encouraged to use the shortcut methods on the {API} module, which abstracts away the
# difference between this service and the {RecurringService}. Henceforth, for extensive
# documentation you should look at the {API} documentation.
#
# The most important difference is that you instantiate a {PaymentService} with the parameters
# that are needed for the call that you will eventually make.
#
# @example
# payment = Adyen::API::PaymentService.new({
# :reference => invoice.id,
# :amount => {
# :currency => 'EUR',
# :value => invoice.amount,
# },
# :shopper => {
# :email => user.email,
# :reference => user.id,
# #:ip => request.,
# },
# :card => {
# :expiry_month => 12,
# :expiry_year => 2012,
# :holder_name => 'Simon Hopper',
# :number => '4444333322221111',
# :cvc => '737'
# }
# })
# response = payment.authorise_payment
# response.authorised? # => true
#
class PaymentService < SimpleSOAPClient
# The Adyen Payment SOAP service endpoint uri.
ENDPOINT_URI = 'https://pal-%s.adyen.com/pal/servlet/soap/Payment'
# @see API.authorise_payment
def authorise_payment
make_payment_request(authorise_payment_request_body, AuthorisationResponse)
end
# @see API.authorise_recurring_payment
def authorise_recurring_payment
make_payment_request(authorise_recurring_payment_request_body, AuthorisationResponse)
end
# @see API.authorise_one_click_payment
def authorise_one_click_payment
make_payment_request(authorise_one_click_payment_request_body, AuthorisationResponse)
end
# @see API.capture_payment
def capture
make_payment_request(capture_request_body, CaptureResponse)
end
# @see API.refund_payment
def refund
make_payment_request(refund_request_body, RefundResponse)
end
# @see API.cancel_payment
def cancel
make_payment_request(cancel_request_body, CancelResponse)
end
# @see API.cancel_or_refund_payment
def cancel_or_refund
make_payment_request(cancel_or_refund_request_body, CancelOrRefundResponse)
end
private
def make_payment_request(data, response_class)
call_webservice_action('authorise', data, response_class)
end
def validate_parameter_value!(param, value)
if value.blank?
raise ArgumentError, "The required parameter `:#{param}' is missing."
end
end
def validate_parameters!(*params)
params.each do |param|
case param
when Symbol
validate_parameter_value!(param, @params[param])
when Hash
param.each do |name, attrs|
validate_parameter_value!(name, @params[name])
attrs.each { |attr| validate_parameter_value!("#{name} => :#{attr}", @params[name][attr]) }
end
end
end
end
def authorise_payment_request_body
validate_parameters!(:merchant_account)
content = card_partial
content << ENABLE_RECURRING_CONTRACTS_PARTIAL if @params[:recurring]
payment_request_body(content)
end
def authorise_recurring_payment_request_body
content = RECURRING_PAYMENT_BODY_PARTIAL % (@params[:recurring_detail_reference] || 'LATEST')
payment_request_body(content)
end
def authorise_one_click_payment_request_body
content = ONE_CLICK_PAYMENT_BODY_PARTIAL % [@params[:recurring_detail_reference], @params[:card][:cvc]]
payment_request_body(content)
end
def payment_request_body(content)
content << amount_partial
content << shopper_partial if @params[:shopper]
LAYOUT % [@params[:merchant_account], @params[:reference], content]
end
def capture_request_body
CAPTURE_LAYOUT % capture_and_refund_params
end
def refund_request_body
REFUND_LAYOUT % capture_and_refund_params
end
def cancel_or_refund_request_body
CANCEL_OR_REFUND_LAYOUT % [@params[:merchant_account], @params[:psp_reference]]
end
def cancel_request_body
CANCEL_LAYOUT % [@params[:merchant_account], @params[:psp_reference]]
end
def capture_and_refund_params
[@params[:merchant_account], @params[:psp_reference], *@params[:amount].values_at(:currency, :value)]
end
def amount_partial
AMOUNT_PARTIAL % @params[:amount].values_at(:currency, :value)
end
def card_partial
validate_parameters!(:card => [:holder_name, :number, :cvc, :expiry_year, :expiry_month])
card = @params[:card].values_at(:holder_name, :number, :cvc, :expiry_year)
card << @params[:card][:expiry_month].to_i
CARD_PARTIAL % card
end
def shopper_partial
@params[:shopper].map { |k, v| SHOPPER_PARTIALS[k] % v }.join("\n")
end
class AuthorisationResponse < Response
ERRORS = {
"validation 101 Invalid card number" => [:number, 'is not a valid creditcard number'],
"validation 103 CVC is not the right length" => [:cvc, 'is not the right length'],
"validation 128 Card Holder Missing" => [:holder_name, 'can’t be blank'],
"validation Couldn't parse expiry year" => [:expiry_year, 'could not be recognized'],
"validation Expiry month should be between 1 and 12 inclusive" => [:expiry_month, 'could not be recognized'],
}
AUTHORISED = 'Authorised'
def self.original_fault_message_for(attribute, message)
if error = ERRORS.find { |_, (a, m)| a == attribute && m == message }
error.first
else
message
end
end
response_attrs :result_code, :auth_code, :refusal_reason, :psp_reference
def success?
super && params[:result_code] == AUTHORISED
end
alias authorized? success?
# @return [Boolean] Returns whether or not the request was valid.
def invalid_request?
!fault_message.nil?
end
# In the case of a validation error, or SOAP fault message, this method will return an
# array describing what attribute failed validation and the accompanying message. If the
# errors is not of the common user validation errors, then the attribute is +:base+ and the
# full original message is returned.
#
# An optional +prefix+ can be given so you can seamlessly integrate this in your
# ActiveRecord model and copy over errors.
#
# @param [String,Symbol] prefix A string that should be used to prefix the error key.
# @return [Array<Symbol, String>] A name-message pair of the attribute with an error.
def error(prefix = nil)
if error = ERRORS[fault_message]
prefix ? ["#{prefix}_#{error[0]}".to_sym, error[1]] : error
else
[:base, fault_message]
end
end
def params
@params ||= xml_querier.xpath('//payment:authoriseResponse/payment:paymentResult') do |result|
{
:psp_reference => result.text('./payment:pspReference'),
:result_code => result.text('./payment:resultCode'),
:auth_code => result.text('./payment:authCode'),
:refusal_reason => (invalid_request? ? fault_message : result.text('./payment:refusalReason'))
}
end
end
end
class ModificationResponse < Response
class << self
# @private
attr_accessor :request_received_value, :base_xpath
end
response_attrs :psp_reference, :response
# This only returns whether or not the request has been successfully received. Check the
# subsequent notification to see if the payment was actually mutated.
def success?
super && params[:response] == self.class.request_received_value
end
def params
@params ||= xml_querier.xpath(self.class.base_xpath) do |result|
{
:psp_reference => result.text('./payment:pspReference'),
:response => result.text('./payment:response')
}
end
end
end
class CaptureResponse < ModificationResponse
self.request_received_value = '[capture-received]'
self.base_xpath = '//payment:captureResponse/payment:captureResult'
end
class RefundResponse < ModificationResponse
self.request_received_value = '[refund-received]'
self.base_xpath = '//payment:refundResponse/payment:refundResult'
end
class CancelResponse < ModificationResponse
self.request_received_value = '[cancel-received]'
self.base_xpath = '//payment:cancelResponse/payment:cancelResult'
end
class CancelOrRefundResponse < ModificationResponse
self.request_received_value = '[cancelOrRefund-received]'
self.base_xpath = '//payment:cancelOrRefundResponse/payment:cancelOrRefundResult'
end
end
end
end