buckaruby 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.
- 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'
|