bambora-client 0.1.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.
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