ach_client 0.5.1

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 (65) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +18 -0
  3. data/.gitignore +9 -0
  4. data/.rubocop.yml +1156 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +10 -0
  7. data/Gemfile +4 -0
  8. data/README.md +388 -0
  9. data/Rakefile +10 -0
  10. data/ach_client.gemspec +58 -0
  11. data/bin/console +93 -0
  12. data/bin/setup +8 -0
  13. data/config/return_codes.yml +761 -0
  14. data/lib/ach_client/abstract/abstract_method_error.rb +3 -0
  15. data/lib/ach_client/helpers/dollars_to_cents.rb +15 -0
  16. data/lib/ach_client/logging/log_provider_job.rb +36 -0
  17. data/lib/ach_client/logging/log_providers/log_provider.rb +22 -0
  18. data/lib/ach_client/logging/log_providers/null_log_provider.rb +14 -0
  19. data/lib/ach_client/logging/log_providers/stdout_log_provider.rb +14 -0
  20. data/lib/ach_client/logging/logging.rb +76 -0
  21. data/lib/ach_client/logging/savon_observer.rb +26 -0
  22. data/lib/ach_client/objects/account_types.rb +32 -0
  23. data/lib/ach_client/objects/responses/ach_response.rb +15 -0
  24. data/lib/ach_client/objects/responses/corrected_ach_response.rb +18 -0
  25. data/lib/ach_client/objects/responses/processing_ach_response.rb +6 -0
  26. data/lib/ach_client/objects/responses/returned_ach_response.rb +16 -0
  27. data/lib/ach_client/objects/responses/settled_ach_response.rb +5 -0
  28. data/lib/ach_client/objects/return_code.rb +20 -0
  29. data/lib/ach_client/objects/return_codes.rb +33 -0
  30. data/lib/ach_client/objects/transaction_types.rb +16 -0
  31. data/lib/ach_client/providers/abstract/ach_batch.rb +23 -0
  32. data/lib/ach_client/providers/abstract/ach_status_checker.rb +21 -0
  33. data/lib/ach_client/providers/abstract/ach_transaction.rb +70 -0
  34. data/lib/ach_client/providers/abstract/company_info.rb +28 -0
  35. data/lib/ach_client/providers/abstract/response_record_processor.rb +15 -0
  36. data/lib/ach_client/providers/abstract/transformer.rb +38 -0
  37. data/lib/ach_client/providers/sftp/account_type_transformer.rb +20 -0
  38. data/lib/ach_client/providers/sftp/ach_batch.rb +97 -0
  39. data/lib/ach_client/providers/sftp/ach_status_checker.rb +146 -0
  40. data/lib/ach_client/providers/sftp/ach_transaction.rb +62 -0
  41. data/lib/ach_client/providers/sftp/nacha_provider.rb +30 -0
  42. data/lib/ach_client/providers/sftp/sftp_provider.rb +124 -0
  43. data/lib/ach_client/providers/sftp/transaction_type_transformer.rb +19 -0
  44. data/lib/ach_client/providers/soap/ach_works/account_type_transformer.rb +17 -0
  45. data/lib/ach_client/providers/soap/ach_works/ach_batch.rb +89 -0
  46. data/lib/ach_client/providers/soap/ach_works/ach_status_checker.rb +86 -0
  47. data/lib/ach_client/providers/soap/ach_works/ach_transaction.rb +80 -0
  48. data/lib/ach_client/providers/soap/ach_works/ach_works.rb +57 -0
  49. data/lib/ach_client/providers/soap/ach_works/company_info.rb +87 -0
  50. data/lib/ach_client/providers/soap/ach_works/correction_details_processor.rb +92 -0
  51. data/lib/ach_client/providers/soap/ach_works/date_formatter.rb +33 -0
  52. data/lib/ach_client/providers/soap/ach_works/response_record_processor.rb +91 -0
  53. data/lib/ach_client/providers/soap/ach_works/transaction_type_transformer.rb +18 -0
  54. data/lib/ach_client/providers/soap/i_check_gateway/account_type_transformer.rb +20 -0
  55. data/lib/ach_client/providers/soap/i_check_gateway/ach_batch.rb +12 -0
  56. data/lib/ach_client/providers/soap/i_check_gateway/ach_status_checker.rb +35 -0
  57. data/lib/ach_client/providers/soap/i_check_gateway/ach_transaction.rb +53 -0
  58. data/lib/ach_client/providers/soap/i_check_gateway/company_info.rb +51 -0
  59. data/lib/ach_client/providers/soap/i_check_gateway/i_check_gateway.rb +39 -0
  60. data/lib/ach_client/providers/soap/i_check_gateway/response_record_processor.rb +59 -0
  61. data/lib/ach_client/providers/soap/i_check_gateway/transaction_type_transformer.rb +8 -0
  62. data/lib/ach_client/providers/soap/soap_provider.rb +47 -0
  63. data/lib/ach_client/version.rb +4 -0
  64. data/lib/ach_client.rb +38 -0
  65. metadata +346 -0
@@ -0,0 +1,80 @@
1
+ module AchClient
2
+ class AchWorks
3
+
4
+ # AchWorks implementation for AchTransaction
5
+ class AchTransaction < Abstract::AchTransaction
6
+
7
+ ##
8
+ # @param super [Array] args from parent class
9
+ # @param customer_id [String] optional identifier for the customer
10
+ def self.arguments
11
+ super + [:customer_id]
12
+ end
13
+
14
+ attr_reader :customer_id
15
+
16
+ # Send this transaction individually to AchWorks
17
+ # @return [String] the front end trace
18
+ def send
19
+ AchClient::AchWorks.wrap_request(
20
+ method: :send_ach_trans,
21
+ message: AchClient::AchWorks::CompanyInfo.build.to_hash.merge({
22
+ InpACHTransRecord: self.to_hash
23
+ }),
24
+ path: [:send_ach_trans_response, :send_ach_trans_result]
25
+ )[:front_end_trace]
26
+ end
27
+
28
+ ##
29
+ # @return [Hash] turns this transaction into a hash that can be sent to
30
+ # AchWorks
31
+ def to_hash
32
+ {
33
+ SSS: AchClient::AchWorks.s_s_s,
34
+ LocID: AchClient::AchWorks.loc_i_d,
35
+ FrontEndTrace: front_end_trace,
36
+ CustomerName: merchant_name,
37
+ CustomerRoutingNo: routing_number.to_s,
38
+ CustomerAcctNo: account_number.to_s,
39
+ OriginatorName: originator_name.try(:first, 16),
40
+ TransactionCode: sec_code,
41
+ CustTransType:
42
+ AchClient::AchWorks::TransactionTypeTransformer.serialize_to_provider_value(
43
+ transaction_type
44
+ ),
45
+ CustomerID: customer_id,
46
+ CustomerAcctType:
47
+ AchClient::AchWorks::AccountTypeTransformer.serialize_to_provider_value(
48
+ self.account_type
49
+ ),
50
+ TransAmount: amount,
51
+ CheckOrTransDate: DateFormatter.format(effective_entry_date),
52
+ EffectiveDate: DateFormatter.format(effective_entry_date),
53
+ Memo: memo.try(:first, 10),
54
+ OpCode: 'S', # Check this
55
+ AccountSet: '1'
56
+ }
57
+ end
58
+
59
+ # AchWorks Ach needs a "FrontEndTrace", for each ACH transaction.
60
+ # These can be used to track the processing of the ACH after it has been
61
+ # submitted.
62
+ # You can use the id of your Ach record
63
+ # It should be unique per ACH
64
+ # The consumer is responsible for ensuring the uniqueness of this value
65
+ # @return [String] the 12 char front end trace
66
+ def front_end_trace
67
+ # I want to stop this before it goes through because AchWorks might
68
+ # just truncate the value, which could result in lost Achs.
69
+ if external_ach_id.length > 11
70
+ raise 'AchWorks requires a FrontEndTrace of 12 chars or less'
71
+ else
72
+ # The front end trace MUST NOT start with a W.
73
+ # Our front end trace starts with a Z.
74
+ # The letter Z is not the letter W.
75
+ "Z#{external_ach_id}"
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,57 @@
1
+ require 'active_support/all'
2
+ require_relative '../soap_provider'
3
+
4
+ module AchClient
5
+ # Namespace class for all things AchWorks
6
+ # Contains class attributes with various initialization settings
7
+ class AchWorks
8
+
9
+ # See concern for functionality shared with other providers that SOAP it up
10
+ include SoapProvider
11
+
12
+ # @return [String] A key that they give you used as a password
13
+ class_attribute :company_key
14
+
15
+ # @return [String] Your user id string, used as a username
16
+ class_attribute :company
17
+
18
+ # @return [String] Another Arbitrary 4 letter code AchWorks gives you...
19
+ class_attribute :loc_i_d
20
+
21
+ # @return [String] Arbitrary 3 letter code AchWorks gives your company
22
+ class_attribute :s_s_s
23
+
24
+ # Handles making request to AchWorks.
25
+ # If the request was successful, returns the response
26
+ # If it is unsuccessful, tries to find the error message and raises it in
27
+ # an exception
28
+ # @param method [Symbol] SOAP operation to call against AchWorks
29
+ # @param message [Hash] The request body
30
+ # @param path [Array<Symbol>] Path to the attributes we care about (and the
31
+ # status field) within the response hash. For example, if you have a burrito
32
+ # but only care about the guacomole, the path to the hash with the guac
33
+ # would be: [:aluminum_foil, :tortilla]
34
+ # @return [Hash] The hash the input path led to within the response hash, if
35
+ # the response was successful.
36
+ def self.wrap_request(method:, message:, path:)
37
+ response = self.request(method: method, message: message)
38
+ if response.success?
39
+ response = path.reduce(response.body) {|r, node| r[node] }
40
+ if response[:status] == 'SUCCESS'
41
+ # It worked! Return the response hash
42
+ response
43
+ else
44
+ # AchWorks likes to keep things interesting by sometimes putting
45
+ # the error messages in the details field instead of errors.
46
+ raise response.try(:[], :errors)
47
+ .try(:[], :string)
48
+ .try(:join, ', ') ||
49
+ response[:details]
50
+ end
51
+ else
52
+ # This would normally raise an exception on its own, but just in case
53
+ raise "#{method} failed due to unknown SOAP fault"
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,87 @@
1
+ module AchClient
2
+ class AchWorks
3
+ # This is the ACHworks "credentials" for your company
4
+ class CompanyInfo < Abstract::CompanyInfo
5
+
6
+ attr_reader :company_key,
7
+ :company,
8
+ :loc_i_d,
9
+ :s_s_s
10
+
11
+ ##
12
+ # @param s_s_s [String] Arbitrary 3 letter code they give your company
13
+ # @param loc_i_d [String] Another Arbitrary 4 letter code they give you...
14
+ # @param company [String] Your user id string, used as a username
15
+ # @param company_key [String] A key that they give you used as a password
16
+ # Since all these fields are generated by them, and don't change, it
17
+ # really seems like they could use just one.
18
+ def initialize(
19
+ company_key:,
20
+ company:,
21
+ loc_i_d:,
22
+ s_s_s:
23
+ )
24
+ @company = company
25
+ @company_key = company_key
26
+ @loc_i_d = loc_i_d
27
+ @s_s_s = s_s_s
28
+ end
29
+
30
+ ##
31
+ # @return [CompanyInfo] instance built from configuration values
32
+ def self.build
33
+ build_from_config([
34
+ :company_key,
35
+ :company,
36
+ :loc_i_d,
37
+ :s_s_s
38
+ ])
39
+ end
40
+
41
+ # Wraps: http://tstsvr.achworks.com/dnet/achws.asmx?op=ConnectionCheck
42
+ # Checks validity of company info
43
+ # @return whether or not the request was successful
44
+ def connection_valid?
45
+ connection_check_request(method: :connection_check)
46
+ end
47
+
48
+ # Wraps: http://tstsvr.achworks.com/dnet/achws.asmx?op=CheckCompanyStatus
49
+ # Checks company status
50
+ # @return whether or not the request was successful
51
+ def company_valid?
52
+ connection_check_request(method: :check_company_status)
53
+ end
54
+
55
+ # Calls both company_valid? and connection_valid?
56
+ # Checks the validity of company info
57
+ # @return whether or not the validity check requests were successful
58
+ def valid?
59
+ connection_valid? && company_valid?
60
+ end
61
+
62
+ ##
63
+ # Build a hash to send to ACHWorks under the InpCompanyInfo XML path
64
+ # @return [Hash] hash to send to ACHWorks
65
+ def to_hash
66
+ {
67
+ InpCompanyInfo: self.instance_variables.map do |var|
68
+ {
69
+ var.to_s.split('@').last.camelize.to_sym =>
70
+ self.instance_variable_get(var)
71
+ }
72
+ end.reduce(&:merge)
73
+ }
74
+ end
75
+
76
+ private
77
+ def connection_check_request(method:)
78
+ AchClient::AchWorks.request(
79
+ method: method,
80
+ message: self.to_hash
81
+ ).body["#{method}_response".to_sym]["#{method}_result".to_sym].include?(
82
+ 'SUCCESS'
83
+ )
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,92 @@
1
+ module AchClient
2
+ class AchWorks
3
+ # Turns the gibberish string that AchWorks gives us for correction returns
4
+ # when possible into meaningful data
5
+ class CorrectionDetailsProcessor
6
+
7
+ # Turns the gibberish string that AchWorks gives us for correction returns
8
+ # when possible into meaningful data
9
+ # @param gibberish [String] the string that AchWorks gave you
10
+ # @return [Hash] a key value pairing of corrected attributes and their
11
+ # values. When possible.
12
+ def self.decipher_correction_details(gibberish)
13
+ # The correction code is the first 3 chars of the gibberish.
14
+ # The meaning of the rest of the giberish depends on the correction
15
+ # code. These meanings are sometimes enumerated in the AchWorks
16
+ # documentation.
17
+ self.send(('decipher_' + gibberish[0..2]).to_sym, gibberish)
18
+ end
19
+
20
+ ##
21
+ # If it looks like we tried to call a function for an unknown correction
22
+ # code, then return the unhanlded correction data hash
23
+ def self.method_missing(method, *args, &block)
24
+ if method.to_s.start_with?('decipher_')
25
+ if (gibberish = args[0]) && gibberish.is_a?(String)
26
+ self.decipher_unknown(gibberish)
27
+ end
28
+ else
29
+ super
30
+ end
31
+ end
32
+
33
+ # CO3: The routing number and the account number were wrong.
34
+ def self.decipher_C03(gibberish)
35
+ {
36
+ routing_number: gibberish[3..11],
37
+ account_number: gibberish[15..31]
38
+ }
39
+ end
40
+
41
+ # Discrepency between AchWorks and standard correction codes:
42
+ # AchWorks: The account number and transaction code were wrong.
43
+ # Everyone else: The account number and account type (checking/saving)
44
+ # were wrong.
45
+ # However, AchWorks indicates that the "transaction code" is only 1
46
+ # character long within their gibberish string. Their transaction
47
+ # codes are usually 3 characters, while their account types are one
48
+ # character. So I will assume that "transaction code" means
49
+ # "account type".
50
+ # C06: The account number and account type were wrong
51
+ def self.decipher_C06(gibberish)
52
+ {
53
+ account_number: gibberish[3..19],
54
+ account_type:
55
+ AchClient::AchWorks::AccountTypeTransformer.deserialize_provider_value(
56
+ gibberish[23]
57
+ )
58
+ }
59
+ end
60
+
61
+ # C07: The account number, routing number, and account type were all
62
+ # incorrect. You really messed this one up.
63
+ # Same issue as above with Transaction Code => Account Type
64
+ # At least they were consistently discrepent.
65
+ def self.decipher_C07(gibberish)
66
+ {
67
+ routing_number: gibberish[3..11],
68
+ account_number: gibberish[12..28],
69
+ account_type:
70
+ AchClient::AchWorks::AccountTypeTransformer.deserialize_provider_value(
71
+ gibberish[29]
72
+ )
73
+ }
74
+ end
75
+
76
+ # The rest of the cases are undocumented. We will expose the raw data
77
+ # given to us, along with a nice note that explains the situation
78
+ # while shaming AchWorks.
79
+ def self.decipher_unknown(gibberish)
80
+ {
81
+ unhandled_correction_data: gibberish[3..-1],
82
+ note: 'AchWorks failed to document this correction code, so we ' +
83
+ 'can\'t tell you what this data means. You might be able to find ' +
84
+ 'out by contacting them. Alternatively, you could check the ' +
85
+ 'AchWorks web console, and match your records against theirs to ' +
86
+ 'see what changed. Enjoy! Let us know what you find, and maybe we' +
87
+ ' can handle this case in the future.'
88
+ }
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,33 @@
1
+ module AchClient
2
+ class AchWorks
3
+ ##
4
+ # For formatting dates for AchWorks
5
+ class DateFormatter
6
+
7
+ ##
8
+ # Formats given date in the manner required by AchWorks
9
+ # The date can be a String or a Date/DateTime.
10
+ # If it is a string it will be given to the DateTime parser
11
+ # Will be formatted like 2016-08-11T09:56:24.35103-04:00
12
+ # @param date [Object] String or Date to format
13
+ # @return [String] formatted datetime
14
+ def self.format(date)
15
+ if date.is_a?(String)
16
+ format_string(date)
17
+ elsif date.respond_to?(:strftime)
18
+ format_date(date)
19
+ else
20
+ raise 'Cannot format date'
21
+ end
22
+ end
23
+
24
+ private_class_method def self.format_string(string)
25
+ format_date(DateTime.parse(string))
26
+ end
27
+
28
+ private_class_method def self.format_date(date)
29
+ date.strftime('%Y-%m-%dT%H:%M:%S.%5N%:z')
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,91 @@
1
+ module AchClient
2
+ class AchWorks
3
+ # Processes individual response records from AchWorks
4
+ class ResponseRecordProcessor < Abstract::ResponseRecordProcessor
5
+
6
+ # Find the response code in the response record and delegate to the
7
+ # appropriate handler method
8
+ # @param record [Hash] AchWorks response hash
9
+ # @return [AchClient::AchResponse] response
10
+ def self.process_response_record(record)
11
+ self.send(('process_' + record[:response_code]).to_sym, record)
12
+ end
13
+
14
+ ##
15
+ # If it looks like we tried to call a function for an unknown response
16
+ # code, then raise an exception so we know what kind of crazy response
17
+ # codes they are sending us
18
+ def self.method_missing(method, *args, &block)
19
+ if method.to_s.start_with?('process_')
20
+ if (record = args[0]) && record.is_a?(Hash)
21
+ raise "Unknown response code #{record[:response_code]}"
22
+ end
23
+ else
24
+ super
25
+ end
26
+ end
27
+
28
+ # 1SNT: The transaction has been sent, but not yet processed
29
+ # @param record [Hash] AchWorks response hash
30
+ # @return [AchClient::ProcessingAchResponse] processing response
31
+ def self.process_1SNT(record)
32
+ AchClient::ProcessingAchResponse.new(
33
+ amount: BigDecimal.new(record[:trans_amount]),
34
+ date: record[:action_date]
35
+ )
36
+ end
37
+
38
+ # 2STL: The transaction is settled. Huzzah!
39
+ # @param record [Hash] AchWorks response hash
40
+ # @return [AchClient::SettledAchResponse] settled response
41
+ def self.process_2STL(record)
42
+ AchClient::SettledAchResponse.new(
43
+ amount: BigDecimal.new(record[:trans_amount]),
44
+ date: record[:action_date]
45
+ )
46
+ end
47
+
48
+ # 3RET: The transaction was returned for some reason (insufficient
49
+ # funds, invalid account, etc)
50
+ # @param record [Hash] AchWorks response hash
51
+ # @return [AchClient::ReturnedAchResponse] returned response
52
+ def self.process_3RET(record)
53
+ AchClient::ReturnedAchResponse.new(
54
+ amount: BigDecimal.new(record[:trans_amount]),
55
+ date: record[:action_date],
56
+ return_code: AchClient::ReturnCodes.find_by(
57
+ code: record[:action_detail][0..2]
58
+ )
59
+ )
60
+ end
61
+
62
+ # 4INT: AchWorks already knows the transaction would result in a
63
+ # return, so they didn't bother sending it to the bank.
64
+ # @param record [Hash] AchWorks response hash
65
+ # @return [AchClient::ReturnedAchResponse] returned response
66
+ def self.process_4INT(record)
67
+ self.process_3RET(record)
68
+ end
69
+
70
+ # 5COR: Corrected account details (new account number, bank buys
71
+ # another bank, etc). You are responsible for updating your records,
72
+ # and making the request with the new info, lest AchWorks will be
73
+ # most displeased.
74
+ # @param record [Hash] AchWorks response hash
75
+ # @return [AchClient::CorrectedAchResponse] corrected response
76
+ def self.process_5COR(record)
77
+ AchClient::CorrectedAchResponse.new(
78
+ amount: BigDecimal.new(record[:trans_amount]),
79
+ date: record[:action_date],
80
+ return_code: AchClient::ReturnCodes.find_by(
81
+ code: record[:action_detail][0..2]
82
+ ),
83
+ corrections: AchClient::AchWorks::CorrectionDetailsProcessor
84
+ .decipher_correction_details(record[:action_detail])
85
+ )
86
+ end
87
+
88
+ # Not pictured: 9BNK
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,18 @@
1
+ module AchClient
2
+ class AchWorks
3
+ ##
4
+ # Transforms TransactionTypes between AchClient class and the string
5
+ # that AchWorks expects
6
+ class TransactionTypeTransformer < AchClient::Transformer
7
+
8
+ # 'C' means Credit, 'D' means Debit
9
+ # @return [Hash {String => Class}] the mapping
10
+ def self.transformer
11
+ {
12
+ 'C' => AchClient::TransactionTypes::Credit,
13
+ 'D' => AchClient::TransactionTypes::Debit
14
+ }
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ module AchClient
2
+ class ICheckGateway
3
+ ##
4
+ # Transforms AccountTypes between AchClient class and the string
5
+ # that ICheckGateway expects
6
+ class AccountTypeTransformer < AchClient::Transformer
7
+ # 'B' means Business, 'P' means Personal
8
+ # 'C' means Checking, 'S' means Savings
9
+ # @return [Hash {String => Class}] the mapping
10
+ def self.transformer
11
+ {
12
+ 'PS' => AchClient::AccountTypes::PersonalSavings,
13
+ 'PC' => AchClient::AccountTypes::PersonalChecking,
14
+ 'BS' => AchClient::AccountTypes::BusinessSavings,
15
+ 'BC' => AchClient::AccountTypes::BusinessChecking
16
+ }
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,12 @@
1
+ module AchClient
2
+ class ICheckGateway
3
+ # Implementation of AchBatch for ICheckGateway
4
+ class AchBatch < Abstract::AchBatch
5
+
6
+ # ICheckGateway does not support ACH batching
7
+ def send_batch
8
+ raise 'ICheckGateway does not support ACH batching'
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,35 @@
1
+ module AchClient
2
+ class ICheckGateway
3
+ # Poll ICheckGateway for status of processed or processing Ach transactions.
4
+ class AchStatusChecker < Abstract::AchStatusChecker
5
+ ##
6
+ # ICheckGateway does not support this
7
+ def self.most_recent
8
+ # In the future, this might just return the last 24 hours or something
9
+ raise 'ICheckGateway does not have a most_recent bucket'
10
+ end
11
+
12
+ # Wrapper for the range response endpoint
13
+ # @return [Hash{String => AchClient::AchResponse}] Hash with confirmation
14
+ # number as the key, AchResponse objects as values
15
+ def self.in_range(start_date:, end_date:)
16
+ AchClient::ICheckGateway.wrap_request(
17
+ method: :pull_transaction_report,
18
+ message: AchClient::ICheckGateway::CompanyInfo.build.to_hash.merge({
19
+ startDate: start_date,
20
+ endDate: end_date
21
+ })
22
+ ).split("\n").select do |record|
23
+ # Ignore credit card swipes if there are any
24
+ record.start_with?('ICHECK')
25
+ end.map do |record|
26
+ {
27
+ record.split('|')[3] =>
28
+ AchClient::ICheckGateway::ResponseRecordProcessor
29
+ .process_response_record(record)
30
+ }
31
+ end.reduce(&:merge)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,53 @@
1
+ module AchClient
2
+ class ICheckGateway
3
+ # ICheckGateway implementation for AchTransaction
4
+ class AchTransaction < Abstract::AchTransaction
5
+
6
+ # Sends this transaction to ICheckGateway
7
+ # If successful, returns a string from the response that seems to be
8
+ # a unique identifier for the transaction from ICheckGateway
9
+ # Raises an exception with as much info as possible if something goes
10
+ # wrong
11
+ # @return [String] a string returned by ICheckGateway - external_ach_id
12
+ def send
13
+ # The response comes back as a | separated list of field values with
14
+ # no header field/keys. It seems that the first column will contain
15
+ # 'APPROVED' if the request was successful. The 8th column is the
16
+ # confirmation number
17
+ response = AchClient::ICheckGateway.wrap_request(
18
+ method: :process_check,
19
+ message: self.to_hash
20
+ ).split('|')
21
+ if response[0] == 'APPROVED'
22
+ # Return the confirmation number
23
+ response[7]
24
+ else
25
+ # Don't have a reliable way of getting the error message, so we will
26
+ # just raise the whole response.
27
+ raise "ICheckGateway ACH Transaction Failure: #{response.join('|')}"
28
+ end
29
+ end
30
+
31
+ ## Turns this transaction into a Hash that can be sent via soap to the
32
+ # provider
33
+ # @return [Hash] payload for ICheckGateway
34
+ def to_hash
35
+ AchClient::ICheckGateway::CompanyInfo.build.to_hash.merge({
36
+ APIMethod: 'ProcessCheck',
37
+ Amount: amount,
38
+ RoutingNumber: routing_number.to_s,
39
+ AccountNumber: account_number.to_s,
40
+ AccountType: AchClient::ICheckGateway::AccountTypeTransformer
41
+ .serialize_to_provider_value(account_type),
42
+ EntryClassCode: sec_code,
43
+ TransactionType:
44
+ AchClient::ICheckGateway::TransactionTypeTransformer
45
+ .serialize_to_provider_value(transaction_type),
46
+ CompanyName: merchant_name,
47
+ Description: memo,
48
+ TransactionDate: effective_entry_date
49
+ })
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,51 @@
1
+ module AchClient
2
+ class ICheckGateway
3
+ # ICheckGateway credentials for your company
4
+ class CompanyInfo < Abstract::CompanyInfo
5
+ attr_reader :api_key,
6
+ :site_i_d,
7
+ :site_key,
8
+ :live
9
+
10
+ ##
11
+ # @param api_key [String] your ICheckGateway API key
12
+ # @param site_i_d [String] your ICheckGateway SiteID
13
+ # @param site_key [String] your ICheckGateway SiteKey
14
+ # @param live [Boolean] "GatewayLiveMode" value
15
+ def initialize(
16
+ api_key:,
17
+ site_i_d:,
18
+ site_key:,
19
+ live:
20
+ )
21
+ @api_key = api_key
22
+ @site_i_d = site_i_d
23
+ @site_key = site_key
24
+ @live = live
25
+ end
26
+
27
+ ##
28
+ # @return [CompanyInfo] instance built from configuration values
29
+ def self.build
30
+ build_from_config([
31
+ :api_key,
32
+ :live,
33
+ :site_i_d,
34
+ :site_key
35
+ ])
36
+ end
37
+
38
+ ##
39
+ # Build a hash to send to ICheckGateway
40
+ # @return [Hash] hash to send to ICheckGateway
41
+ def to_hash
42
+ {
43
+ SiteID: @site_i_d,
44
+ SiteKey: @site_key,
45
+ APIKey: @api_key,
46
+ GatewayLiveMode: @live ? '1' : '0'
47
+ }
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,39 @@
1
+ module AchClient
2
+ # Wrapper class for all things ICheckGateway
3
+ class ICheckGateway
4
+ include SoapProvider
5
+
6
+ # @return [String] Site identifier from ICheckGateway
7
+ class_attribute :site_i_d
8
+
9
+ # @return [String] Site key from ICheckGateway
10
+ class_attribute :site_key
11
+
12
+ # @return [String] API key from ICheckGateway
13
+ class_attribute :api_key
14
+
15
+ # @return [Boolean] ICheckGateway GatewayLiveMode setting
16
+ # ICheckGateway uses their production environment for test/sandbox accounts
17
+ # Set this to false if you don't want your transactions to actually complete
18
+ class_attribute :live
19
+
20
+ ## Wraps SOAP request with exception handling and pulls relevent hash out of
21
+ # response
22
+ # @param method [Symbol] SOAP action to call
23
+ # @param message [Hash] SOAP message to send
24
+ # @return [Hash] ICheckGateway response
25
+ def self.wrap_request(method:, message:)
26
+ response = AchClient::ICheckGateway.request(
27
+ method: method,
28
+ message: message
29
+ )
30
+ if response.success?
31
+ response.body["#{method}_response".to_sym]["#{method}_result".to_sym]
32
+ else
33
+ # This happens when something goes wrong within the actual HTTP
34
+ # request before it gets to the soap processing.
35
+ raise 'Unknown ICheckGateway SOAP fault'
36
+ end
37
+ end
38
+ end
39
+ end