ach_client 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.codeclimate.yml +18 -0
- data/.gitignore +9 -0
- data/.rubocop.yml +1156 -0
- data/.ruby-version +1 -0
- data/.travis.yml +10 -0
- data/Gemfile +4 -0
- data/README.md +388 -0
- data/Rakefile +10 -0
- data/ach_client.gemspec +58 -0
- data/bin/console +93 -0
- data/bin/setup +8 -0
- data/config/return_codes.yml +761 -0
- data/lib/ach_client/abstract/abstract_method_error.rb +3 -0
- data/lib/ach_client/helpers/dollars_to_cents.rb +15 -0
- data/lib/ach_client/logging/log_provider_job.rb +36 -0
- data/lib/ach_client/logging/log_providers/log_provider.rb +22 -0
- data/lib/ach_client/logging/log_providers/null_log_provider.rb +14 -0
- data/lib/ach_client/logging/log_providers/stdout_log_provider.rb +14 -0
- data/lib/ach_client/logging/logging.rb +76 -0
- data/lib/ach_client/logging/savon_observer.rb +26 -0
- data/lib/ach_client/objects/account_types.rb +32 -0
- data/lib/ach_client/objects/responses/ach_response.rb +15 -0
- data/lib/ach_client/objects/responses/corrected_ach_response.rb +18 -0
- data/lib/ach_client/objects/responses/processing_ach_response.rb +6 -0
- data/lib/ach_client/objects/responses/returned_ach_response.rb +16 -0
- data/lib/ach_client/objects/responses/settled_ach_response.rb +5 -0
- data/lib/ach_client/objects/return_code.rb +20 -0
- data/lib/ach_client/objects/return_codes.rb +33 -0
- data/lib/ach_client/objects/transaction_types.rb +16 -0
- data/lib/ach_client/providers/abstract/ach_batch.rb +23 -0
- data/lib/ach_client/providers/abstract/ach_status_checker.rb +21 -0
- data/lib/ach_client/providers/abstract/ach_transaction.rb +70 -0
- data/lib/ach_client/providers/abstract/company_info.rb +28 -0
- data/lib/ach_client/providers/abstract/response_record_processor.rb +15 -0
- data/lib/ach_client/providers/abstract/transformer.rb +38 -0
- data/lib/ach_client/providers/sftp/account_type_transformer.rb +20 -0
- data/lib/ach_client/providers/sftp/ach_batch.rb +97 -0
- data/lib/ach_client/providers/sftp/ach_status_checker.rb +146 -0
- data/lib/ach_client/providers/sftp/ach_transaction.rb +62 -0
- data/lib/ach_client/providers/sftp/nacha_provider.rb +30 -0
- data/lib/ach_client/providers/sftp/sftp_provider.rb +124 -0
- data/lib/ach_client/providers/sftp/transaction_type_transformer.rb +19 -0
- data/lib/ach_client/providers/soap/ach_works/account_type_transformer.rb +17 -0
- data/lib/ach_client/providers/soap/ach_works/ach_batch.rb +89 -0
- data/lib/ach_client/providers/soap/ach_works/ach_status_checker.rb +86 -0
- data/lib/ach_client/providers/soap/ach_works/ach_transaction.rb +80 -0
- data/lib/ach_client/providers/soap/ach_works/ach_works.rb +57 -0
- data/lib/ach_client/providers/soap/ach_works/company_info.rb +87 -0
- data/lib/ach_client/providers/soap/ach_works/correction_details_processor.rb +92 -0
- data/lib/ach_client/providers/soap/ach_works/date_formatter.rb +33 -0
- data/lib/ach_client/providers/soap/ach_works/response_record_processor.rb +91 -0
- data/lib/ach_client/providers/soap/ach_works/transaction_type_transformer.rb +18 -0
- data/lib/ach_client/providers/soap/i_check_gateway/account_type_transformer.rb +20 -0
- data/lib/ach_client/providers/soap/i_check_gateway/ach_batch.rb +12 -0
- data/lib/ach_client/providers/soap/i_check_gateway/ach_status_checker.rb +35 -0
- data/lib/ach_client/providers/soap/i_check_gateway/ach_transaction.rb +53 -0
- data/lib/ach_client/providers/soap/i_check_gateway/company_info.rb +51 -0
- data/lib/ach_client/providers/soap/i_check_gateway/i_check_gateway.rb +39 -0
- data/lib/ach_client/providers/soap/i_check_gateway/response_record_processor.rb +59 -0
- data/lib/ach_client/providers/soap/i_check_gateway/transaction_type_transformer.rb +8 -0
- data/lib/ach_client/providers/soap/soap_provider.rb +47 -0
- data/lib/ach_client/version.rb +4 -0
- data/lib/ach_client.rb +38 -0
- 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
|