bambora-client 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/linter.yml +13 -0
  3. data/.github/workflows/ruby.yml +34 -0
  4. data/.gitignore +11 -0
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +108 -0
  7. data/.ruby-version +1 -0
  8. data/CODE_OF_CONDUCT.md +74 -0
  9. data/Gemfile +8 -0
  10. data/Gemfile.lock +93 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +198 -0
  13. data/Rakefile +8 -0
  14. data/bambora-client.gemspec +55 -0
  15. data/bin/console +12 -0
  16. data/bin/setup +8 -0
  17. data/lib/bambora/adapters/json_response.rb +7 -0
  18. data/lib/bambora/adapters/multipart_mixed_request.rb +23 -0
  19. data/lib/bambora/adapters/query_string_response.rb +20 -0
  20. data/lib/bambora/adapters/response.rb +44 -0
  21. data/lib/bambora/bank/adapters/payment_profile_response.rb +36 -0
  22. data/lib/bambora/bank/batch_report_messages.rb +81 -0
  23. data/lib/bambora/bank/batch_report_resource.rb +79 -0
  24. data/lib/bambora/bank/builders/payment_profile_params.rb +39 -0
  25. data/lib/bambora/bank/payment_profile_resource.rb +81 -0
  26. data/lib/bambora/builders/batch_payment_csv.rb +41 -0
  27. data/lib/bambora/builders/headers.rb +43 -0
  28. data/lib/bambora/builders/www_form_parameters.rb +33 -0
  29. data/lib/bambora/builders/xml_request_body.rb +19 -0
  30. data/lib/bambora/client.rb +215 -0
  31. data/lib/bambora/client/version.rb +7 -0
  32. data/lib/bambora/factories/response_adapter_factory.rb +21 -0
  33. data/lib/bambora/rest/batch_payment_file_upload_client.rb +58 -0
  34. data/lib/bambora/rest/client.rb +63 -0
  35. data/lib/bambora/rest/json_client.rb +109 -0
  36. data/lib/bambora/rest/www_form_client.rb +38 -0
  37. data/lib/bambora/rest/xml_client.rb +35 -0
  38. data/lib/bambora/v1/batch_payment_resource.rb +52 -0
  39. data/lib/bambora/v1/payment_resource.rb +82 -0
  40. data/lib/bambora/v1/profile_resource.rb +85 -0
  41. metadata +256 -0
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bambora
4
+ module Builders
5
+ class BatchPaymentCSV
6
+ class << self
7
+ ##
8
+ # Return a CSV with one transaction per row.
9
+ #
10
+ # @see https://dev.na.bambora.com/docs/references/batch_payment/
11
+ #
12
+ # @example
13
+ # Bambora::Builders::BatchPaymentCSV.build([{
14
+ # super_type: 'E',
15
+ # transaction_type: 'D',
16
+ # institution_number: 12345,
17
+ # transit_number: 123,
18
+ # account_number: 1223456789,
19
+ # amount: 10000
20
+ # reference_nubmer: 1234,
21
+ # recipient_name: 'Hup Podling',
22
+ # customer_code: '02355E2e58Bf488EAB4EaFAD7083dB6A',
23
+ # dynamic_description: 'The Skeksis',
24
+ # }])
25
+ #
26
+ # # => "E,D,12345,123,123456789,10000,1234,Hup Podling,02355E2e58Bf488EAB4EaFAD7083dB6A,The Skeksis\n"
27
+ #
28
+ # @param transactions [Array] an array of transaciton hashes.
29
+ #
30
+ # @return [String], a CSV as a string
31
+ def build(transactions)
32
+ CSV.generate(row_sep: "\r\n") do |csv|
33
+ transactions.each do |transaction|
34
+ csv << transaction.values
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bambora
4
+ module Builders
5
+ ##
6
+ # Builds Headers for HTTP requests.
7
+ class Headers
8
+ attr_reader :api_key, :merchant_id, :sub_merchant_id, :content_type
9
+
10
+ ##
11
+ # Initialize a new Headers object.
12
+ #
13
+ # @param options[:api_key] [String]
14
+ # @param options[:merchant_id] [String]
15
+ # @param options[:sub_merchant_id] [String] optional.
16
+ # @param options[:content_type] [String] optional.
17
+ def initialize(options = {})
18
+ options.each do |key, value|
19
+ instance_variable_set("@#{key}", value)
20
+ end
21
+ end
22
+
23
+ ##
24
+ # Builds a header object.
25
+ #
26
+ # @return [Hash]
27
+ def build
28
+ headers = {
29
+ 'Authorization' => "Passcode #{passcode}",
30
+ }
31
+ headers['Content-Type'] = content_type if content_type
32
+ headers['Sub-Merchant-Id'] = sub_merchant_id if sub_merchant_id
33
+ headers
34
+ end
35
+
36
+ private
37
+
38
+ def passcode
39
+ Base64.encode64("#{merchant_id}:#{api_key}").delete("\n")
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bambora
4
+ module Builders
5
+ ##
6
+ # Builds WWW URL Encoded request parameters from a Hash
7
+ class WWWFormParameters
8
+ attr_reader :body
9
+
10
+ ##
11
+ # Initiallze a new WWWFormParameter
12
+ #
13
+ # @params body [Hash]
14
+ def initialize(body:)
15
+ @body = body
16
+ end
17
+
18
+ ##
19
+ # Convert a hash to url-encoded query parameters.
20
+ #
21
+ # @return [String]
22
+ def to_s
23
+ URI.encode_www_form(sanitized_body)
24
+ end
25
+
26
+ private
27
+
28
+ def sanitized_body
29
+ body.reject { |_, val| val.to_s.empty? }
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bambora
4
+ module Builders
5
+ ##
6
+ # Builds an XML request body from a Hash
7
+ class XMLRequestBody
8
+ attr_reader :body
9
+
10
+ def initialize(body:)
11
+ @body = body
12
+ end
13
+
14
+ def to_s
15
+ "<?xml version='1.0' encoding='utf-8'?>#{Gyoku.xml(request: body)}"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,215 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Standard Libraries
4
+ require 'base64'
5
+ require 'cgi'
6
+ require 'csv'
7
+ require 'json'
8
+
9
+ # Gems
10
+ require 'faraday' # HTTP Wraper
11
+ require 'gyoku' # XML Builder
12
+ require 'multiparty' # Multipart/mixed requests
13
+
14
+ require 'bambora/client/version'
15
+
16
+ # Adapters
17
+ require 'bambora/adapters/response'
18
+ require 'bambora/adapters/json_response'
19
+ require 'bambora/adapters/multipart_mixed_request'
20
+ require 'bambora/adapters/query_string_response'
21
+ require 'bambora/bank/adapters/payment_profile_response'
22
+
23
+ # Builders
24
+ require 'bambora/builders/headers'
25
+ require 'bambora/builders/xml_request_body'
26
+ require 'bambora/builders/www_form_parameters'
27
+ require 'bambora/builders/batch_payment_csv'
28
+ require 'bambora/bank/builders/payment_profile_params'
29
+
30
+ # Factories
31
+ require 'bambora/factories/response_adapter_factory'
32
+
33
+ # Clients
34
+ require 'bambora/rest/client'
35
+ require 'bambora/rest/batch_payment_file_upload_client'
36
+ require 'bambora/rest/json_client'
37
+ require 'bambora/rest/www_form_client'
38
+ require 'bambora/rest/xml_client'
39
+
40
+ # Resources
41
+ require 'bambora/v1/batch_payment_resource'
42
+ require 'bambora/v1/payment_resource'
43
+ require 'bambora/v1/profile_resource'
44
+ require 'bambora/bank/payment_profile_resource'
45
+ require 'bambora/bank/batch_report_messages'
46
+ require 'bambora/bank/batch_report_resource'
47
+
48
+ module Bambora
49
+ ##
50
+ # The Client class is used to initialize Resource objects that can make requests to the Bambora API.
51
+ class Client
52
+ class Error < StandardError; end
53
+
54
+ attr_reader :base_url, :merchant_id, :scripts_api_base_url, :sub_merchant_id
55
+
56
+ # Initialze a new Bambora::Client.
57
+ #
58
+ # @example
59
+ #
60
+ # client = Bambora::Client.new do |c|
61
+ # c.base_url = ENV.fetch('BAMBORA_BASE_URL')
62
+ # c.scripts_api_base_url = ENV.fetch('BAMBORA_SCRIPTS_BASE_URL')
63
+ # c.merchant_id = ENV.fetch('BAMBORA_MERCHANT_ID')
64
+ # c.sub_merchant_id = ENV.fetch('BAMBORA_SUB_MERCHANT_ID')
65
+ # end
66
+ #
67
+ # @param options[base_url] [String] Bambora Base URL
68
+ # @param options[merchant_id] [String] The Merchant ID for this request.
69
+ # @param options[sub_merchant_id] [String] The Sub-Merchant ID for this request.
70
+ def initialize(options = {})
71
+ options.each do |key, value|
72
+ instance_variable_set("@#{key}", value)
73
+ end
74
+
75
+ yield(self) if block_given?
76
+ end
77
+
78
+ # Retrieve a client to make requests to the Profiles endpoints.
79
+ #
80
+ # @example
81
+ # profiles = client.profiles
82
+ # profiles.delete(customer_code: '02355E2e58Bf488EAB4EaFAD7083dB6A')
83
+ #
84
+ # @param api_key [String] API key for the profiles endpoint.
85
+ #
86
+ # @return [Bambora::V1::ProfileResource]
87
+ def profiles(api_key:)
88
+ @profiles ||= Bambora::V1::ProfileResource.new(client: json_client, api_key: api_key)
89
+ end
90
+
91
+ # Retrieve a client to make requests to the Payments endpoints.
92
+ #
93
+ # @example
94
+ # payments = client.profiles(api_key: <yourapikey>)
95
+ # payments.create(
96
+ # {
97
+ # amount: 50,
98
+ # payment_method: 'card',
99
+ # card: {
100
+ # name: 'Hup Podling',
101
+ # number: '4504481742333',
102
+ # expiry_month: '12',
103
+ # expiry_year: '20',
104
+ # cvd: '123',
105
+ # },
106
+ # },
107
+ # )
108
+ #
109
+ # @param api_key [String] API key for the payments endpoint.
110
+ #
111
+ # @return [Bambora::V1::PaymentResource]
112
+ def payments(api_key:)
113
+ @payments ||= Bambora::V1::PaymentResource.new(client: json_client, api_key: api_key)
114
+ end
115
+
116
+ # Retrieve a client to make requests to the Bank Payment Profiles endpoints.
117
+ #
118
+ # @example
119
+ # profiles = client.bank_profiles(api_key: <yourapikey>)
120
+ #
121
+ # data = {
122
+ # customer_code: '1234',
123
+ # bank_account_type: 'CA',
124
+ # bank_account_holder: 'All-Maudra Mayrin',
125
+ # institution_number: '123',
126
+ # branch_number: '12345',
127
+ # account_number: '123456789',
128
+ # name: 'Brea Princess of Vapra',
129
+ # email_address: 'brea@theresistance.com',
130
+ # phone_number: '1231231234',
131
+ # address_1: 'The Castle',
132
+ # city: "Ha'rar",
133
+ # postal_code: 'H0H 0H0',
134
+ # province: 'Vapra',
135
+ # country: 'Thra',
136
+ # sub_merchant_id: 1,
137
+ # }
138
+ #
139
+ # profiles.create(data)
140
+ #
141
+ # @param api_key [String] API key for the bank profiles endpoint.
142
+ #
143
+ # @return [Bambora::Bank::PaymentProfileResource]
144
+ def bank_profiles(api_key:)
145
+ @bank_profiles ||= Bambora::Bank::PaymentProfileResource.new(client: www_form_client, api_key: api_key)
146
+ end
147
+
148
+ # Retrieve a client to make requests to the batch report endpoint.
149
+ #
150
+ # @example
151
+ # batch_reports = client.batch_reports(api_key: <yourapikey>)
152
+ #
153
+ # data = {
154
+ # rpt_filter_by_1: 'batch_id',
155
+ # rpt_filter_value_1: 1,
156
+ # rpt_operation_type_1: 'EQ',
157
+ # rpt_from_date_time: '2019-12-18 00:00:00',
158
+ # rpt_to_date_time: '2019-12-18 23:59:59',
159
+ # service_name: 'BatchPaymentsEFT',
160
+ # }
161
+ #
162
+ # batch_reports.show(data)
163
+ #
164
+ # @param api_key [String] API key for the bank profiles endpoint.
165
+ #
166
+ # @return [Bambora::Bank::PaymentProfileResource]
167
+ def batch_reports(api_key:)
168
+ @batch_reports = Bambora::Bank::BatchReportResource.new(
169
+ client: xml_client,
170
+ api_key: api_key,
171
+ )
172
+ end
173
+
174
+ def batch_payments(api_key:)
175
+ @batch_payments ||= Bambora::V1::BatchPaymentResource.new(
176
+ client: batch_payment_file_upload_client,
177
+ api_key: api_key,
178
+ )
179
+ end
180
+
181
+ private
182
+
183
+ def json_client
184
+ @json_client ||= Bambora::Rest::JSONClient.new(
185
+ base_url: base_url,
186
+ merchant_id: merchant_id,
187
+ sub_merchant_id: sub_merchant_id,
188
+ )
189
+ end
190
+
191
+ def www_form_client
192
+ @www_form_client ||= Bambora::Rest::WWWFormClient.new(
193
+ base_url: scripts_api_base_url,
194
+ merchant_id: merchant_id,
195
+ sub_merchant_id: sub_merchant_id,
196
+ )
197
+ end
198
+
199
+ def batch_payment_file_upload_client
200
+ @batch_payment_file_upload_client ||= Bambora::Rest::BatchPaymentFileUploadClient.new(
201
+ base_url: base_url,
202
+ merchant_id: merchant_id,
203
+ sub_merchant_id: sub_merchant_id,
204
+ )
205
+ end
206
+
207
+ def xml_client
208
+ @xml_client ||= Bambora::Rest::XMLClient.new(
209
+ base_url: scripts_api_base_url,
210
+ merchant_id: merchant_id,
211
+ sub_merchant_id: sub_merchant_id,
212
+ )
213
+ end
214
+ end
215
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bambora
4
+ class Client
5
+ VERSION = '0.1.0'
6
+ end
7
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bambora
4
+ ##
5
+ # Selects an adapter for parsing an HTTP response body
6
+ class ResponseAdapterFactory
7
+ class << self
8
+ def for(response)
9
+ content_type = response.headers['Content-Type'].split(';').first
10
+ case content_type
11
+ when 'application/json'
12
+ Bambora::JSONResponse.new(response)
13
+ when 'text/html'
14
+ # Currently, the only endpoint that responds with text/html is /scripts/payment_profiles.asp
15
+ Bambora::Bank::Adapters::PaymentProfileResponse.new(response)
16
+ else raise Bambora::Client::Error, "Unknown Content Type: #{content_type}. Response Body: #{response.body}"
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bambora
4
+ module Rest
5
+ class BatchPaymentFileUploadClient < Bambora::Rest::Client
6
+ CONTENT_DISPOSITION = 'form-data'
7
+ CONTENT_TYPES = {
8
+ text_plain: 'text/plain',
9
+ application_json: 'application/json',
10
+ }.freeze
11
+ FILE_TYPE_HEADER = { 'FileType' => 'STD' }.freeze
12
+
13
+ # @param args[:file_contents] [String] CSV file contents
14
+ # @param args[:options] [Hash] Request Parameters as documented by Bambora.
15
+ # @param args[:api_key] [String]
16
+ def post(args = {})
17
+ init_payload(args[:file_contents], args[:options])
18
+ parse_response_body(
19
+ super(
20
+ path: args[:path],
21
+ body: @payload.body,
22
+ headers: build_headers(api_key: args[:api_key]),
23
+ ),
24
+ ).to_h
25
+ end
26
+
27
+ private
28
+
29
+ def init_payload(file_contents, options)
30
+ return @payload if @payload
31
+
32
+ @payload = Bambora::Adapters::MultipartMixedRequest.new(
33
+ multipart_args: {
34
+ criteria: {
35
+ content_disposition: CONTENT_DISPOSITION,
36
+ content_type: CONTENT_TYPES[:application_json],
37
+ content: options.to_json,
38
+ },
39
+ data: {
40
+ filename: "merchant_#{sub_merchant_id}.txt",
41
+ content_type: CONTENT_TYPES[:text_plain],
42
+ content: file_contents,
43
+ },
44
+ },
45
+ )
46
+ end
47
+
48
+ def build_headers(api_key:)
49
+ headers = Bambora::Builders::Headers.new(
50
+ content_type: @payload.content_type,
51
+ api_key: api_key,
52
+ merchant_id: merchant_id,
53
+ ).build
54
+ headers.merge(FILE_TYPE_HEADER)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bambora
4
+ module Rest
5
+ ##
6
+ # A basic client for making REST requests.
7
+ class Client
8
+ attr_reader :base_url, :merchant_id, :sub_merchant_id
9
+
10
+ # Initialze a client that makes REST requests.
11
+ #
12
+ # @example
13
+ # rest_client = Bambora::Rest::Client.new(
14
+ # base_url: ENV.fetch('BAMBORA_BASE_URL'),
15
+ # merchant_id: ENV.fetch('BAMBORA_MERCHANT_ID'),
16
+ # sub_merchant_id: ENV.fetch('BAMBORA_SUB_MERCHANT_ID'),
17
+ # )
18
+ def initialize(options = {})
19
+ options.each do |key, value|
20
+ instance_variable_set("@#{key}", value)
21
+ end
22
+ yield(self) if block_given?
23
+ end
24
+
25
+ protected
26
+
27
+ def get(path:, params:, headers:)
28
+ connection.get(path, params, headers)
29
+ end
30
+
31
+ def post(path:, body:, headers:)
32
+ connection.post(path, body, headers)
33
+ end
34
+
35
+ def delete(path:, headers:)
36
+ connection.delete(path) do |req|
37
+ req.headers = headers
38
+ end
39
+ end
40
+
41
+ def connection
42
+ @connection ||= Faraday.new(url: base_url) do |faraday|
43
+ faraday.request :multipart
44
+ faraday.request :url_encoded
45
+ faraday.adapter :excon
46
+ end
47
+ end
48
+
49
+ def build_headers(api_key:, content_type: nil)
50
+ Bambora::Builders::Headers.new(
51
+ content_type: content_type,
52
+ api_key: api_key,
53
+ merchant_id: merchant_id,
54
+ sub_merchant_id: sub_merchant_id,
55
+ ).build
56
+ end
57
+
58
+ def parse_response_body(response)
59
+ Bambora::ResponseAdapterFactory.for(response).to_h
60
+ end
61
+ end
62
+ end
63
+ end