buckaruby 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/.rubocop.yml +49 -0
- data/.travis.yml +6 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +137 -0
- data/Rakefile +8 -0
- data/buckaruby.gemspec +26 -0
- data/lib/buckaruby/action.rb +7 -0
- data/lib/buckaruby/currency.rb +5 -0
- data/lib/buckaruby/exception.rb +54 -0
- data/lib/buckaruby/gateway.rb +204 -0
- data/lib/buckaruby/iban.rb +25 -0
- data/lib/buckaruby/ideal.rb +16 -0
- data/lib/buckaruby/language.rb +8 -0
- data/lib/buckaruby/operation.rb +6 -0
- data/lib/buckaruby/payment_method.rb +14 -0
- data/lib/buckaruby/request.rb +193 -0
- data/lib/buckaruby/response.rb +198 -0
- data/lib/buckaruby/signature.rb +30 -0
- data/lib/buckaruby/support/case_insensitive_hash.rb +35 -0
- data/lib/buckaruby/transaction_status.rb +9 -0
- data/lib/buckaruby/transaction_type.rb +8 -0
- data/lib/buckaruby/urls.rb +6 -0
- data/lib/buckaruby/version.rb +3 -0
- data/lib/buckaruby.rb +20 -0
- data/spec/buckaruby/gateway_spec.rb +529 -0
- data/spec/buckaruby/iban_spec.rb +73 -0
- data/spec/buckaruby/signature_spec.rb +62 -0
- data/spec/buckaruby/support/case_insensitive_hash_spec.rb +72 -0
- data/spec/spec_helper.rb +11 -0
- metadata +129 -0
@@ -0,0 +1,193 @@
|
|
1
|
+
require 'bigdecimal'
|
2
|
+
require 'cgi'
|
3
|
+
require 'date'
|
4
|
+
require 'logger'
|
5
|
+
require 'net/http'
|
6
|
+
require 'openssl'
|
7
|
+
require 'uri'
|
8
|
+
|
9
|
+
module Buckaruby
|
10
|
+
# Base class for any request.
|
11
|
+
class Request
|
12
|
+
def initialize(options)
|
13
|
+
@options = options
|
14
|
+
|
15
|
+
@logger = defined?(Rails) ? Rails.logger : Logger.new(STDOUT)
|
16
|
+
end
|
17
|
+
|
18
|
+
def execute(options)
|
19
|
+
uri = URI.parse(api_url)
|
20
|
+
uri.query = "op=#{options[:operation]}" if options[:operation]
|
21
|
+
|
22
|
+
raw_response = post_buckaroo(uri, build_request_data(options))
|
23
|
+
response = parse_response(raw_response)
|
24
|
+
|
25
|
+
@logger.debug("[execute] response: #{response.inspect}")
|
26
|
+
|
27
|
+
return response
|
28
|
+
end
|
29
|
+
|
30
|
+
def build_request_params(_options)
|
31
|
+
raise NotImplementedError
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def post_buckaroo(uri, params)
|
37
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
38
|
+
if uri.scheme == "https"
|
39
|
+
http.use_ssl = true
|
40
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
41
|
+
end
|
42
|
+
|
43
|
+
raw_response = http.post(uri.request_uri, post_data(params))
|
44
|
+
return raw_response.body
|
45
|
+
# Try to catch some common exceptions Net::HTTP might raise
|
46
|
+
rescue Errno::ETIMEDOUT, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, EOFError, IOError, SocketError,
|
47
|
+
Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::OpenTimeout, Net::ProtocolError, Net::ReadTimeout,
|
48
|
+
OpenSSL::SSL::SSLError => ex
|
49
|
+
raise ConnectionException, ex
|
50
|
+
end
|
51
|
+
|
52
|
+
def build_request_data(options)
|
53
|
+
params = { brq_websitekey: @options[:website] }
|
54
|
+
|
55
|
+
params.merge!(build_request_params(options))
|
56
|
+
|
57
|
+
params[:add_buckaruby] = "Buckaruby #{Buckaruby::VERSION}"
|
58
|
+
|
59
|
+
# Sign the data with our secret key.
|
60
|
+
params[:brq_signature] = Signature.generate_signature(params, @options)
|
61
|
+
|
62
|
+
return params
|
63
|
+
end
|
64
|
+
|
65
|
+
def post_data(params)
|
66
|
+
return params.map { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join("&")
|
67
|
+
end
|
68
|
+
|
69
|
+
def parse_response(body)
|
70
|
+
query = CGI.parse(body)
|
71
|
+
query.each { |key, value| query[key] = value.first }
|
72
|
+
return query
|
73
|
+
end
|
74
|
+
|
75
|
+
def test?
|
76
|
+
return @options[:mode] == :test
|
77
|
+
end
|
78
|
+
|
79
|
+
def api_url
|
80
|
+
return test? ? Urls::TEST_URL : Urls::PRODUCTION_URL
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Base class for a transaction request.
|
85
|
+
class TransactionRequest < Request
|
86
|
+
def execute(options)
|
87
|
+
super(options.merge(operation: Operation::TRANSACTION_REQUEST))
|
88
|
+
end
|
89
|
+
|
90
|
+
def build_request_params(options)
|
91
|
+
params = {
|
92
|
+
brq_payment_method: options[:payment_method],
|
93
|
+
brq_culture: options[:culture] || Language::DUTCH,
|
94
|
+
brq_currency: options[:currency] || Currency::EURO,
|
95
|
+
brq_amount: BigDecimal.new(options[:amount].to_s).to_s("F"),
|
96
|
+
brq_invoicenumber: options[:invoicenumber]
|
97
|
+
}
|
98
|
+
|
99
|
+
params.merge!(build_transaction_request_params(options))
|
100
|
+
|
101
|
+
params[:brq_clientip] = options[:client_ip] if options[:client_ip]
|
102
|
+
params[:brq_description] = options[:description] if options[:description]
|
103
|
+
params[:brq_return] = options[:return_url] if options[:return_url]
|
104
|
+
|
105
|
+
return params
|
106
|
+
end
|
107
|
+
|
108
|
+
def build_transaction_request_params(_options)
|
109
|
+
raise NotImplementedError
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Request for a creating a new transaction.
|
114
|
+
class SetupTransactionRequest < TransactionRequest
|
115
|
+
def execute(options)
|
116
|
+
super(options.merge(action: Action::PAY))
|
117
|
+
end
|
118
|
+
|
119
|
+
def build_transaction_request_params(options)
|
120
|
+
params = {}
|
121
|
+
|
122
|
+
case options[:payment_method]
|
123
|
+
when PaymentMethod::IDEAL
|
124
|
+
params.merge!(
|
125
|
+
brq_service_ideal_action: Action::PAY,
|
126
|
+
brq_service_ideal_issuer: options[:payment_issuer],
|
127
|
+
brq_service_ideal_version: "2"
|
128
|
+
)
|
129
|
+
when PaymentMethod::SEPA_DIRECT_DEBIT
|
130
|
+
params.merge!(
|
131
|
+
brq_service_sepadirectdebit_action: Action::PAY,
|
132
|
+
brq_service_sepadirectdebit_customeriban: options[:account_iban],
|
133
|
+
brq_service_sepadirectdebit_customeraccountname: options[:account_name],
|
134
|
+
brq_service_sepadirectdebit_collectdate: (Date.today + 5).strftime("%Y-%m-%d")
|
135
|
+
)
|
136
|
+
|
137
|
+
if options[:account_bic]
|
138
|
+
params[:brq_service_sepadirectdebit_customerbic] = options[:account_bic]
|
139
|
+
end
|
140
|
+
|
141
|
+
if options[:mandate_reference]
|
142
|
+
params.merge!(
|
143
|
+
brq_service_sepadirectdebit_action: [Action::PAY, Action::EXTRA_INFO].join(","),
|
144
|
+
brq_service_sepadirectdebit_mandatereference: options[:mandate_reference],
|
145
|
+
brq_service_sepadirectdebit_mandatedate: Date.today.strftime("%Y-%m-%d")
|
146
|
+
)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
params[:brq_startrecurrent] = options[:recurring] if options[:recurring]
|
151
|
+
|
152
|
+
return params
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# Request for a creating a recurrent transaction.
|
157
|
+
class RecurrentTransactionRequest < TransactionRequest
|
158
|
+
def execute(options)
|
159
|
+
super(options.merge(action: Action::PAY_RECURRENT))
|
160
|
+
end
|
161
|
+
|
162
|
+
def build_transaction_request_params(options)
|
163
|
+
params = {}
|
164
|
+
|
165
|
+
key = :"brq_service_#{options[:payment_method]}_action"
|
166
|
+
params[key] = Action::PAY_RECURRENT
|
167
|
+
|
168
|
+
# Indicate that this is a request without user redirection to a webpage.
|
169
|
+
# This is needed to make recurrent payments working.
|
170
|
+
params[:brq_channel] = "backoffice"
|
171
|
+
|
172
|
+
params[:brq_originaltransaction] = options[:transaction_id]
|
173
|
+
|
174
|
+
return params
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# Request for getting the status of a transaction.
|
179
|
+
class StatusRequest < Request
|
180
|
+
def execute(options)
|
181
|
+
super(options.merge(operation: Operation::TRANSACTION_STATUS))
|
182
|
+
end
|
183
|
+
|
184
|
+
def build_request_params(options)
|
185
|
+
params = {}
|
186
|
+
|
187
|
+
params[:brq_transaction] = options[:transaction_id] if options[:transaction_id]
|
188
|
+
params[:brq_payment] = options[:payment_id] if options[:payment_id]
|
189
|
+
|
190
|
+
return params
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
@@ -0,0 +1,198 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
module Buckaruby
|
4
|
+
# Base class for any response.
|
5
|
+
class Response
|
6
|
+
attr_reader :params
|
7
|
+
|
8
|
+
def initialize(response, options)
|
9
|
+
@params = Support::CaseInsensitiveHash.new(response)
|
10
|
+
|
11
|
+
verify_signature!(response, options)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def verify_signature!(response, options)
|
17
|
+
if params[:brq_apiresult] != "Fail"
|
18
|
+
sent_signature = params[:brq_signature]
|
19
|
+
generated_signature = Signature.generate_signature(response, options)
|
20
|
+
|
21
|
+
if sent_signature != generated_signature
|
22
|
+
raise SignatureException, sent_signature, generated_signature
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Base class for a transaction response.
|
29
|
+
class TransactionResponse < Response
|
30
|
+
def account_bic
|
31
|
+
case payment_method
|
32
|
+
when PaymentMethod::IDEAL
|
33
|
+
return params[:brq_service_ideal_consumerbic]
|
34
|
+
when PaymentMethod::SEPA_DIRECT_DEBIT
|
35
|
+
return params[:brq_service_sepadirectdebit_customerbic]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def account_iban
|
40
|
+
case payment_method
|
41
|
+
when PaymentMethod::IDEAL
|
42
|
+
return params[:brq_service_ideal_consumeriban]
|
43
|
+
when PaymentMethod::SEPA_DIRECT_DEBIT
|
44
|
+
return params[:brq_service_sepadirectdebit_customeriban]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def account_name
|
49
|
+
case payment_method
|
50
|
+
when PaymentMethod::IDEAL
|
51
|
+
return params[:brq_service_ideal_consumername] || params[:brq_customer_name]
|
52
|
+
when PaymentMethod::SEPA_DIRECT_DEBIT
|
53
|
+
return params[:brq_service_sepadirectdebit_customername] || params[:brq_customer_name]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def collect_date
|
58
|
+
if payment_method == PaymentMethod::SEPA_DIRECT_DEBIT
|
59
|
+
return parse_date(params[:brq_service_sepadirectdebit_collectdate])
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def invoicenumber
|
64
|
+
return params[:brq_invoicenumber]
|
65
|
+
end
|
66
|
+
|
67
|
+
def mandate_reference
|
68
|
+
if payment_method == PaymentMethod::SEPA_DIRECT_DEBIT
|
69
|
+
return params[:brq_service_sepadirectdebit_mandatereference]
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def payment_id
|
74
|
+
return params[:brq_payment]
|
75
|
+
end
|
76
|
+
|
77
|
+
def payment_method
|
78
|
+
return parse_payment_method(params[:brq_payment_method] || params[:brq_transaction_method])
|
79
|
+
end
|
80
|
+
|
81
|
+
def redirect_url
|
82
|
+
return params[:brq_redirecturl]
|
83
|
+
end
|
84
|
+
|
85
|
+
def refund_transaction_id
|
86
|
+
return params[:brq_relatedtransaction_refund]
|
87
|
+
end
|
88
|
+
|
89
|
+
def reversal_transaction_id
|
90
|
+
return params[:brq_relatedtransaction_reversal]
|
91
|
+
end
|
92
|
+
|
93
|
+
def timestamp
|
94
|
+
return parse_time(params[:brq_timestamp])
|
95
|
+
end
|
96
|
+
|
97
|
+
def transaction_id
|
98
|
+
return params[:brq_transactions]
|
99
|
+
end
|
100
|
+
|
101
|
+
def transaction_type
|
102
|
+
if params[:brq_transaction_type] && !params[:brq_transaction_type].empty?
|
103
|
+
# See http://support.buckaroo.nl/index.php/Transactietypes
|
104
|
+
case params[:brq_transaction_type]
|
105
|
+
when 'C001', 'C002', 'C004', 'C021', 'C043', 'C044', 'C046', 'C090', 'V001', 'V002', 'V010', 'V021', 'V090'
|
106
|
+
return TransactionType::PAYMENT
|
107
|
+
when 'C005', 'V014', 'V031', 'V032', 'V034', 'V043', 'V044', 'V046', 'V094'
|
108
|
+
return TransactionType::PAYMENT_RECURRENT
|
109
|
+
when 'C079', 'C080', 'C082', 'C092', 'C101', 'C102', 'C121', 'C500', 'V067', 'V068', 'V070', 'V079', 'V080', 'V082', 'V092', 'V101', 'V102', 'V110'
|
110
|
+
return TransactionType::REFUND
|
111
|
+
when 'C501', 'C502', 'C562', 'V111', 'V131', 'V132', 'V134', 'V143', 'V144', 'V146'
|
112
|
+
return TransactionType::REVERSAL
|
113
|
+
end
|
114
|
+
else
|
115
|
+
# Fallback when transaction type is not known (cancelling credit card)
|
116
|
+
return TransactionType::PAYMENT
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def transaction_status
|
121
|
+
# See http://support.buckaroo.nl/index.php/Statuscodes
|
122
|
+
case params[:brq_statuscode]
|
123
|
+
when '190'
|
124
|
+
return TransactionStatus::SUCCESS
|
125
|
+
when '490', '491', '492'
|
126
|
+
return TransactionStatus::FAILED
|
127
|
+
when '690'
|
128
|
+
return TransactionStatus::REJECTED
|
129
|
+
when '790', '791'
|
130
|
+
return TransactionStatus::PENDING
|
131
|
+
when '890', '891'
|
132
|
+
return TransactionStatus::CANCELLED
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def to_h
|
137
|
+
hash = {
|
138
|
+
account_bic: account_bic,
|
139
|
+
account_iban: account_iban,
|
140
|
+
account_name: account_name,
|
141
|
+
collect_date: collect_date,
|
142
|
+
invoicenumber: invoicenumber,
|
143
|
+
mandate_reference: mandate_reference,
|
144
|
+
payment_id: payment_id,
|
145
|
+
payment_method: payment_method,
|
146
|
+
refund_transaction_id: refund_transaction_id,
|
147
|
+
reversal_transaction_id: reversal_transaction_id,
|
148
|
+
timestamp: timestamp,
|
149
|
+
transaction_id: transaction_id,
|
150
|
+
transaction_type: transaction_type,
|
151
|
+
transaction_status: transaction_status
|
152
|
+
}.reject { |_key, value| value.nil? }
|
153
|
+
|
154
|
+
return hash
|
155
|
+
end
|
156
|
+
|
157
|
+
private
|
158
|
+
|
159
|
+
def parse_date(date)
|
160
|
+
return date ? Date.strptime(date, '%Y-%m-%d') : nil
|
161
|
+
end
|
162
|
+
|
163
|
+
def parse_time(time)
|
164
|
+
return time ? Time.strptime(time, '%Y-%m-%d %H:%M:%S') : nil
|
165
|
+
end
|
166
|
+
|
167
|
+
def parse_payment_method(method)
|
168
|
+
return method ? method.downcase : nil
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# Base class for a transaction response via the API.
|
173
|
+
class TransactionApiResponse < TransactionResponse
|
174
|
+
def initialize(response, options)
|
175
|
+
super(response, options)
|
176
|
+
|
177
|
+
if params[:brq_apiresult].nil? || params[:brq_apiresult] == "Fail"
|
178
|
+
raise ApiException, params
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# Response when creating a new transaction.
|
184
|
+
class SetupTransactionResponse < TransactionApiResponse
|
185
|
+
end
|
186
|
+
|
187
|
+
# Response when creating a recurrent transaction.
|
188
|
+
class RecurrentTransactionResponse < TransactionApiResponse
|
189
|
+
end
|
190
|
+
|
191
|
+
# Response when getting the status of a transaction.
|
192
|
+
class StatusResponse < TransactionApiResponse
|
193
|
+
end
|
194
|
+
|
195
|
+
# Response when verifying the Buckaroo callback.
|
196
|
+
class CallbackResponse < TransactionResponse
|
197
|
+
end
|
198
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'digest'
|
2
|
+
|
3
|
+
module Buckaruby
|
4
|
+
# Calculate a signature based on the parameters of the payment request or response.
|
5
|
+
# -> see BPE 3.0 Gateway NVP, chapter 4 'Digital Signature'
|
6
|
+
class Signature
|
7
|
+
def self.generate_signature(params, options)
|
8
|
+
secret = options[:secret]
|
9
|
+
hash_method = options[:hash_method]
|
10
|
+
|
11
|
+
case hash_method
|
12
|
+
when :sha1
|
13
|
+
return Digest::SHA1.hexdigest(generate_signature_string(params, secret))
|
14
|
+
when :sha256
|
15
|
+
return Digest::SHA256.hexdigest(generate_signature_string(params, secret))
|
16
|
+
when :sha512
|
17
|
+
return Digest::SHA512.hexdigest(generate_signature_string(params, secret))
|
18
|
+
else
|
19
|
+
raise ArgumentError, "Invalid hash method provided: #{hash_method}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.generate_signature_string(params, secret)
|
24
|
+
sign_params = params.select { |key, _value| key.to_s.upcase.start_with?("BRQ_", "ADD_", "CUST_") && key.to_s.casecmp("BRQ_SIGNATURE").nonzero? }
|
25
|
+
string = sign_params.sort_by { |p| p.first.downcase }.map { |param| "#{param[0]}=#{param[1]}" }.join
|
26
|
+
string << secret
|
27
|
+
return string
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Buckaruby
|
2
|
+
module Support
|
3
|
+
# The case insensitive Hash is a Hash with case insensitive keys that
|
4
|
+
# can also be accessed by using a Symbol.
|
5
|
+
class CaseInsensitiveHash < Hash
|
6
|
+
def initialize(constructor = {})
|
7
|
+
if constructor.is_a?(Hash)
|
8
|
+
super()
|
9
|
+
update(constructor)
|
10
|
+
else
|
11
|
+
super(constructor)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def [](key)
|
16
|
+
super(convert_key(key))
|
17
|
+
end
|
18
|
+
|
19
|
+
def []=(key, value)
|
20
|
+
super(convert_key(key), value)
|
21
|
+
end
|
22
|
+
|
23
|
+
protected
|
24
|
+
|
25
|
+
def update(hash)
|
26
|
+
hash.each_pair { |key, value| self[convert_key(key)] = value }
|
27
|
+
end
|
28
|
+
|
29
|
+
def convert_key(key)
|
30
|
+
string = key.is_a?(Symbol) ? key.to_s : key
|
31
|
+
return string.downcase
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/buckaruby.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require_relative 'buckaruby/support/case_insensitive_hash'
|
2
|
+
|
3
|
+
require_relative 'buckaruby/action'
|
4
|
+
require_relative 'buckaruby/currency'
|
5
|
+
require_relative 'buckaruby/ideal'
|
6
|
+
require_relative 'buckaruby/language'
|
7
|
+
require_relative 'buckaruby/operation'
|
8
|
+
require_relative 'buckaruby/payment_method'
|
9
|
+
require_relative 'buckaruby/transaction_status'
|
10
|
+
require_relative 'buckaruby/transaction_type'
|
11
|
+
require_relative 'buckaruby/urls'
|
12
|
+
|
13
|
+
require_relative 'buckaruby/exception'
|
14
|
+
require_relative 'buckaruby/gateway'
|
15
|
+
require_relative 'buckaruby/iban'
|
16
|
+
require_relative 'buckaruby/request'
|
17
|
+
require_relative 'buckaruby/response'
|
18
|
+
require_relative 'buckaruby/signature'
|
19
|
+
|
20
|
+
require_relative 'buckaruby/version'
|