ach_client 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
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