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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+ lib = File.expand_path('lib', __dir__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'bambora/client/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'bambora-client'
8
+ spec.version = Bambora::Client::VERSION
9
+ spec.authors = ['Cassidy K']
10
+ spec.email = ['hello@cassidy.codes', 'tech@himama.com']
11
+
12
+ spec.summary = 'A thread-safe client for the Bambora/Beanstream API.'
13
+ spec.description = 'The official beanstream-ruby gem is not thread-safe. This thread-safe client works in '\
14
+ 'environments like Sidekiq and Puma.'
15
+ spec.homepage = 'https://github.com/HiMamaInc/bambora-client'
16
+ spec.license = 'MIT'
17
+
18
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
19
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
20
+ if spec.respond_to?(:metadata)
21
+ spec.metadata['allowed_push_host'] = "https://rubygems.org"
22
+
23
+ spec.metadata['homepage_uri'] = spec.homepage
24
+ spec.metadata['source_code_uri'] = 'https://github.com/HiMamaInc/bambora-client'
25
+ spec.metadata['changelog_uri'] = 'https://github.com/HiMamaInc/bambora-client/releases'
26
+ else
27
+ raise 'RubyGems 2.0 or newer is required to protect against ' \
28
+ 'public gem pushes.'
29
+ end
30
+
31
+ # Specify which files should be added to the gem when it is released.
32
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
33
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
34
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
35
+ end
36
+ spec.bindir = 'exe'
37
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
38
+ spec.require_paths = ['lib']
39
+
40
+ spec.required_ruby_version = '>= 2.4.6'
41
+
42
+ spec.add_dependency 'excon', '< 1.0'
43
+ spec.add_dependency 'faraday', '< 1.0'
44
+ spec.add_dependency 'gyoku', '~> 1.0'
45
+ spec.add_dependency 'multiparty', '~> 0'
46
+
47
+ spec.add_development_dependency 'bundler', '~> 2'
48
+ spec.add_development_dependency 'pry', '~> 0.12.0'
49
+ spec.add_development_dependency 'pry-byebug', '~> 3.7'
50
+ spec.add_development_dependency 'rake', '~> 10.0'
51
+ spec.add_development_dependency 'rspec', '~> 3.0'
52
+ spec.add_development_dependency 'rspec_junit_formatter', '~> 0.4.1'
53
+ spec.add_development_dependency 'rubocop', '~> 0.74.0'
54
+ spec.add_development_dependency 'webmock', '~> 3.7'
55
+ end
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'bambora/client'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ require 'pry'
12
+ Pry.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bambora
4
+ ##
5
+ # Parses a JSON response into a Hash
6
+ class JSONResponse < Response; end
7
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bambora
4
+ module Adapters
5
+ ##
6
+ # Creates headers and a body for a multipart/mixed request with a file and a JSON body.
7
+ class MultipartMixedRequest
8
+ attr_reader :multiparty
9
+
10
+ def initialize(options = {})
11
+ @multiparty = Multiparty.new { |party| party.parts = options[:multipart_args] }
12
+ end
13
+
14
+ def content_type
15
+ multiparty.header.sub(/^Content-Type: /, '').strip
16
+ end
17
+
18
+ def body
19
+ "#{multiparty.body}\r\n"
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bambora
4
+ ##
5
+ # Parses a query string response into a Hash
6
+ class QueryStringResponse < Response
7
+ def to_h
8
+ parsed_response = super
9
+ return error_response if parsed_response.values.flatten.empty? # We didn't get a query string back.
10
+
11
+ parsed_response.each_with_object({}) { |(key, val), obj| obj[key] = val.length == 1 ? val.first : val }
12
+ end
13
+
14
+ private
15
+
16
+ def parser
17
+ CGI
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bambora
4
+ ##
5
+ # Parses a response into a Hash. Uses JSON to parse by default.
6
+ class Response
7
+ DEFAULT_PARSER = JSON
8
+
9
+ attr_reader :response
10
+
11
+ def initialize(response)
12
+ @response = response
13
+ end
14
+
15
+ def to_h
16
+ deep_transform_keys_in_object(parser.parse(response.body), &:to_sym)
17
+ rescue JSON::ParserError
18
+ error_response
19
+ end
20
+
21
+ private
22
+
23
+ def deep_transform_keys_in_object(object, &block)
24
+ case object
25
+ when Hash
26
+ object.each_with_object({}) do |(key, value), result|
27
+ result[yield(key)] = deep_transform_keys_in_object(value, &block)
28
+ end
29
+ when Array
30
+ object.map { |e| deep_transform_keys_in_object(e, &block) }
31
+ else
32
+ object
33
+ end
34
+ end
35
+
36
+ def error_response
37
+ { status: response.status, body: response.body }
38
+ end
39
+
40
+ def parser
41
+ DEFAULT_PARSER
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bambora
4
+ module Bank
5
+ module Adapters
6
+ ##
7
+ # Transforms hash keys from camelCase to snake_case and strips vendor-specific prefixes.
8
+ class PaymentProfileResponse
9
+ attr_reader :response
10
+
11
+ def initialize(response)
12
+ @response = response
13
+ end
14
+
15
+ def to_h
16
+ parsed_query_string.each_with_object({}) do |(key, val), obj|
17
+ obj[transform(key)] = val
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def parsed_query_string
24
+ Bambora::QueryStringResponse.new(response).to_h
25
+ end
26
+
27
+ def transform(camel_case_word)
28
+ word = camel_case_word.to_s
29
+ word.gsub!(/([a-z])([A-Z\d])/, '\1_\2')
30
+ word.downcase!
31
+ word.sub(/^ord_/, '').to_sym
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bambora
4
+ module Bank
5
+ module BatchReportMessages
6
+ ##
7
+ # Adds message text to the response as per the Bambora Docs:
8
+ # https://help.na.bambora.com/hc/en-us/articles/115010510248-Batch-reporting
9
+ MESSAGES = {
10
+ '1' => 'Invalid bank number',
11
+ '2' => 'Invalid branch number',
12
+ '3' => 'Invalid account number',
13
+ '4' => 'Invalid transaction amount',
14
+ '5' => 'Reference number too long',
15
+ '6' => 'Invalid due date',
16
+ '7' => 'Due date out of valid date range',
17
+ '8' => 'Customer name truncated to 32 characters',
18
+ '9' => 'Customer name missing',
19
+ '10' => 'Duplicate transaction matching bank account',
20
+ '11' => 'Zero, negative or non-numeric amount',
21
+ '12' => 'Invalid bank and/or branch number',
22
+ '13' => 'Payee/drawee name cannot be spaces',
23
+ '14' => 'Invalid payment code',
24
+ '15' => 'Invalid transaction type',
25
+ '16' => 'Account Closed',
26
+ '17' => 'NSF – Debit declined due to insufficient funds.',
27
+ '18' => 'Transaction rejected by Bank',
28
+ '19' => 'Invalid bank, branch, or account number',
29
+ '20' => 'Refused by payor',
30
+ '21' => 'Funds not cleared',
31
+ '22' => 'Account Frozen',
32
+ '23' => 'Payment Stopped',
33
+ '24' => 'Transaction Cancelled',
34
+ '25' => 'Cannot Trace',
35
+ '26' => 'Incorrect Payor/Payee Name',
36
+ '27' => 'Payor/Payee Deceased',
37
+ '28' => 'Invalid transit routing number',
38
+ '29' => 'Invalid Account Type',
39
+ '30' => 'Transaction type not permitted',
40
+ '31' => 'No Checking Privileges',
41
+ '33' => 'Edit Reject',
42
+ '35' => 'Reserved Return Code',
43
+ '36' => 'Payment Recalled',
44
+ '38' => 'Not in accordance with agreement – Personal',
45
+ '39' => 'Agreement revoked – Personal',
46
+ '40' => 'No pre-notification – Personal',
47
+ '41' => 'Not in accordance with agreement – Business',
48
+ '42' => 'Agreement revoked – Business',
49
+ '43' => 'No pre-notification – Business',
50
+ '44' => 'Customer Initiated Return Credit Only',
51
+ '45' => 'Currency/Account Mismatch',
52
+ '46' => 'No Debit Allowed',
53
+ '47' => 'Interbank – Returned Item',
54
+ '48' => 'Routing as entered, account modified',
55
+ '49' => 'Routing as entered, repair of account unknown',
56
+ '50' => 'Routing as entered, account unknown',
57
+ '51' => 'Routing number modified, account as entered',
58
+ '52' => 'Routing number modified, account modified',
59
+ '53' => 'Routing number modified, repair of account unknown',
60
+ '54' => 'Routing number modified, account unknown',
61
+ '55' => 'ACH Unavailable for account',
62
+ '56' => 'Customer code invalid/missing payment info',
63
+ '58' => 'Profile status is closed or disabled',
64
+ '59' => 'Invalid SEC code',
65
+ '60' => 'Invalid Account Identifier',
66
+ '61' => 'Invalid Account Identifier',
67
+ '62' => 'Reference Number is Missing',
68
+ '63' => 'Invalid Customer Country Code',
69
+ '64' => 'Invalid Bank Country Code',
70
+ '65' => 'Invalid Bank Name',
71
+ '66' => 'Bank Name is Missing',
72
+ '67' => 'Addendum not allowed, too long, or has invalid characters',
73
+ '68' => 'Invalid Bank Descriptor',
74
+ '69' => 'Invalid Customer Name',
75
+ '70' => 'Transaction rejected - contact support',
76
+ '71' => 'Refund Request by End Customer',
77
+ '72' => 'Blocked due to a Notice of Change',
78
+ }.freeze
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bambora
4
+ module Bank
5
+ ##
6
+ # For making requests to the /scripts/reporting/report.aspx endpoint
7
+ #
8
+ # @see https://dev.na.bambora.com/docs/guides/batch_payment/report/
9
+ class BatchReportResource
10
+ include Bambora::Bank::BatchReportMessages
11
+
12
+ DEFAULT_REQUEST_PARAMS = {
13
+ rpt_format: 'JSON',
14
+ rpt_version: '2.0',
15
+ session_source: 'external',
16
+ }.freeze
17
+
18
+ attr_reader :client, :api_key, :sub_path, :version
19
+
20
+ ##
21
+ # Instantiate an interface to make requests against Bambora's Profiles API.
22
+ #
23
+ # @example
24
+ #
25
+ # client = Bambora::Rest::XMLClient(base_url: '...', merchant_id: '...')
26
+ # profiles = Bambora::Bank::BatchReportResource(client: client, api_key: '...')
27
+ #
28
+ # # Start making requests ...
29
+ #
30
+ # @param client [Bambora::Rest::XMLClient] An instance of Bambora::Rest::XMLClient, used to make network requests.
31
+ # @param api_key [String] An API key for this endpoint. This is also known as the "Pass Code"
32
+ # @param version [String] The Service Version you are requesting from the server.
33
+ def initialize(client:, api_key:)
34
+ @client = client
35
+ @api_key = api_key
36
+ @sub_path = '/scripts/reporting/report.aspx'
37
+ end
38
+
39
+ ##
40
+ # Create a Bank Payment Profile
41
+ #
42
+ # @example
43
+ # data = {
44
+ # rpt_filter_by_1: 'batch_id',
45
+ # rpt_filter_value_1: 1,
46
+ # rpt_operation_type_1: 'EQ',
47
+ # rpt_from_date_time: '2019-12-18 00:00:00',
48
+ # rpt_to_date_time: '2019-12-18 23:59:59',
49
+ # service_name: 'BatchPaymentsEFT',
50
+ # }
51
+ #
52
+ # payment_profile_resource.show(data)
53
+ #
54
+ # @params profile_data [Hash] with values as noted in the example.
55
+ def show(report_data)
56
+ add_messages_to_response(
57
+ client.post(path: sub_path, body: batch_report_body(report_data)),
58
+ )
59
+ end
60
+
61
+ private
62
+
63
+ def add_messages_to_response(response)
64
+ response.dig(:response, :record).map! do |record|
65
+ record.merge!(messages: record[:messageId].split(',').map { |id| MESSAGES[id] })
66
+ end
67
+ response
68
+ end
69
+
70
+ def batch_report_body(request_data)
71
+ DEFAULT_REQUEST_PARAMS.merge(request_data).merge(
72
+ merchant_id: client.merchant_id,
73
+ pass_code: api_key,
74
+ sub_merchant_id: client.sub_merchant_id,
75
+ )
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bambora
4
+ module Bank
5
+ module Builders
6
+ ##
7
+ # Builds a request body for the Bank Payment Profile endpoint from a Hash
8
+ class PaymentProfileParams
9
+ CONTACT_PARAMS =
10
+ %w[name email_address phone_number address_1 address_2 city postal_code province country].freeze
11
+
12
+ class << self
13
+ ##
14
+ # Converts a snake_case hash to camelCase keys with vendor-specific prefixes.
15
+ # See tests for examples.
16
+ #
17
+ # @params params [Hash]
18
+ def build(params)
19
+ params.each_with_object({}) do |(key, value), obj|
20
+ obj[transform_key(key)] = value
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def transform_key(key)
27
+ key = key.to_s
28
+ key = "ord_#{key}" if CONTACT_PARAMS.include?(key)
29
+
30
+ key.split('_').map.with_index do |word, index|
31
+ word.capitalize! unless index.zero?
32
+ word
33
+ end.join
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bambora
4
+ module Bank
5
+ ##
6
+ # For making requests to the /scripts/payment_profile.asp endpoint
7
+ #
8
+ # @see https://help.na.bambora.com/hc/en-us/articles/115010346067-Secure-Payment-Profiles-Batch-Payments
9
+ class PaymentProfileResource
10
+ DEFAULT_VERSION = 1.0
11
+ DEFAULT_RESPONSE_FORMAT = 'QS'
12
+ CREATE_OPERATION_TYPE = 'N'
13
+
14
+ attr_reader :client, :api_key, :sub_path, :version
15
+
16
+ ##
17
+ # Instantiate an interface to make requests against Bambora's Profiles API.
18
+ #
19
+ # @example
20
+ #
21
+ # client = Bambora::Rest::WWWFormClient(base_url: '...', merchant_id: '...')
22
+ # profiles = Bambora::Bank::PaymentProfileResource(client: client, api_key: '...')
23
+ #
24
+ # # Start making requests ...
25
+ #
26
+ # @param client [Bambora::Rest::WWWFormClient] An instance of Bambora::Rest::WWWFormClient, used to make network
27
+ # requests.
28
+ # @param api_key [String] An API key for this endpoint. This is also known as the "Pass Code"
29
+ # @param version [String] The Service Version you are requesting from the server.
30
+ def initialize(client:, api_key:, version: DEFAULT_VERSION)
31
+ @client = client
32
+ @api_key = api_key
33
+ @version = version
34
+ @sub_path = '/scripts/payment_profile.asp'
35
+ end
36
+
37
+ ##
38
+ # Create a Bank Payment Profile
39
+ #
40
+ # @example
41
+ # data = {
42
+ # customer_code: '1234',
43
+ # bank_account_type: 'CA',
44
+ # bank_account_holder: 'All-Maudra Mayrin',
45
+ # institution_number: '123',
46
+ # branch_number: '12345',
47
+ # account_number: '123456789',
48
+ # name: 'Hup Podling',
49
+ # email_address: 'Brea Princess of Vapra',
50
+ # phone_number: '1231231234',
51
+ # address_1: 'The Castle',
52
+ # city: "Ha'rar",
53
+ # postal_code: 'H0H 0H0',
54
+ # province: 'Vapra',
55
+ # country: 'Thra',
56
+ # }
57
+ #
58
+ # payment_profile_resource.create(data)
59
+ #
60
+ # @params profile_data [Hash] with values as noted in the example.
61
+ def create(profile_data)
62
+ client.post(path: sub_path, body: payment_profile_body(profile_data))
63
+ end
64
+
65
+ private
66
+
67
+ def payment_profile_body(profile_data)
68
+ Bambora::Bank::Builders::PaymentProfileParams.build(
69
+ profile_data.merge(
70
+ pass_code: api_key,
71
+ merchant_id: client.merchant_id,
72
+ sub_merchant_id: client.sub_merchant_id,
73
+ service_version: version,
74
+ response_format: DEFAULT_RESPONSE_FORMAT,
75
+ operation_type: CREATE_OPERATION_TYPE,
76
+ ),
77
+ )
78
+ end
79
+ end
80
+ end
81
+ end