buckaruby 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,9 @@
1
+ module Buckaruby
2
+ module TransactionStatus
3
+ SUCCESS = 1
4
+ FAILED = 2
5
+ REJECTED = 3
6
+ CANCELLED = 4
7
+ PENDING = 5
8
+ end
9
+ end
@@ -0,0 +1,8 @@
1
+ module Buckaruby
2
+ module TransactionType
3
+ PAYMENT = 1
4
+ PAYMENT_RECURRENT = 2
5
+ REFUND = 3
6
+ REVERSAL = 4
7
+ end
8
+ end
@@ -0,0 +1,6 @@
1
+ module Buckaruby
2
+ module Urls
3
+ TEST_URL = "https://testcheckout.buckaroo.nl/nvp/"
4
+ PRODUCTION_URL = "https://checkout.buckaroo.nl/nvp/"
5
+ end
6
+ end
@@ -0,0 +1,3 @@
1
+ module Buckaruby
2
+ VERSION = "1.0.0"
3
+ 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'