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.
- 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
|