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