ach_builder 0.0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/.rspec +2 -0
- data/Gemfile +5 -0
- data/MIT-LICENSE +20 -0
- data/README.md +41 -0
- data/Rakefile +1 -0
- data/ach_builder.gemspec +23 -0
- data/lib/ach/batch/control.rb +19 -0
- data/lib/ach/batch/header.rb +24 -0
- data/lib/ach/batch.rb +37 -0
- data/lib/ach/component.rb +78 -0
- data/lib/ach/entry.rb +31 -0
- data/lib/ach/file/control.rb +15 -0
- data/lib/ach/file/header.rb +28 -0
- data/lib/ach/file.rb +44 -0
- data/lib/ach/formatter.rb +80 -0
- data/lib/ach/record.rb +65 -0
- data/lib/ach/validations.rb +36 -0
- data/lib/ach/version.rb +3 -0
- data/lib/ach_builder.rb +39 -0
- data/spec/batch_spec.rb +51 -0
- data/spec/file_spec.rb +73 -0
- data/spec/formatter_spec.rb +22 -0
- data/spec/record_spec.rb +34 -0
- data/spec/spec_helper.rb +8 -0
- metadata +92 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Artem Kuzko
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
#ACH-Builder
|
2
|
+
|
3
|
+
Ruby helper for creating ACH files. It's API is designed to be as flexible as possible.
|
4
|
+
|
5
|
+
Inspired by
|
6
|
+
|
7
|
+
[ach](http://github.com/jm81/ach) - Ruby helper
|
8
|
+
|
9
|
+
[ACH::Builder](http://github.com/camerb/ACH-Builder) - Perl module
|
10
|
+
|
11
|
+
with similar functionality
|
12
|
+
|
13
|
+
##Example
|
14
|
+
# attributes for records may be passed as parameters, as well as modified in block
|
15
|
+
# these attributes will be passed to all inner entities in a cascade way, if required
|
16
|
+
file = File.new(:company_id => '11-11111', :company_name => 'MY COMPANY') do
|
17
|
+
immediate_dest_name 'COMMERCE BANK'
|
18
|
+
immediate_origin '123123123'
|
19
|
+
immediate_oreigin_name 'MYCOMPANY'
|
20
|
+
|
21
|
+
['WEB', 'TEL'].each do |code|
|
22
|
+
batch(:entry_class_code => code, :company_entry_descr => 'TV-TELCOM') do
|
23
|
+
effective_date Time.now.strftime('%y%m%d')
|
24
|
+
origin_dfi_id "00000000"
|
25
|
+
entry :customer_name => 'JOHN SMITH',
|
26
|
+
:customer_acct => '61242882282',
|
27
|
+
:amount => '2501',
|
28
|
+
:routing_number => '010010101',
|
29
|
+
:bank_account => '103030030'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
file.valid? # => false
|
35
|
+
file.errors # => {"ACH::File::Header#1"=>{:immediate_dest=>"is required"}}
|
36
|
+
file.header.immediate_dest = '123123123'
|
37
|
+
file.write('ach_01.txt')
|
38
|
+
|
39
|
+
##Copyright
|
40
|
+
|
41
|
+
Copyright (c) 2011 Artem Kuzko, released under the MIT license
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/ach_builder.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "ach/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "ach_builder"
|
7
|
+
s.version = ACH::VERSION
|
8
|
+
s.authors = ["Artem Kuzko"]
|
9
|
+
s.email = ["a.kuzko@gmail.com"]
|
10
|
+
s.homepage = "http://github.com/akuzko/ach_builder"
|
11
|
+
s.summary = "Ruby tools for building ACH files"
|
12
|
+
s.description = "Ruby tools for building ACH (Automated Clearing House) files"
|
13
|
+
|
14
|
+
#s.rubyforge_project = "ach_builder"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_development_dependency "rspec", ">= 2.0.0"
|
22
|
+
s.add_runtime_dependency "active_support", ">= 2.3.0"
|
23
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module ACH
|
2
|
+
class Batch::Control < Record
|
3
|
+
fields :record_type,
|
4
|
+
:service_class_code,
|
5
|
+
:entry_count,
|
6
|
+
:entry_hash,
|
7
|
+
:total_debit_amount,
|
8
|
+
:total_credit_amount,
|
9
|
+
:company_id,
|
10
|
+
:authen_code,
|
11
|
+
:bank_6,
|
12
|
+
:origin_dfi_id,
|
13
|
+
:batch_number
|
14
|
+
|
15
|
+
defaults :record_type => 8,
|
16
|
+
:authen_code => '',
|
17
|
+
:bank_6 => ''
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module ACH
|
2
|
+
class Batch::Header < Record
|
3
|
+
fields :record_type,
|
4
|
+
:service_class_code,
|
5
|
+
:company_name,
|
6
|
+
:company_note_data,
|
7
|
+
:company_id,
|
8
|
+
:entry_class_code,
|
9
|
+
:company_entry_descr,
|
10
|
+
:date,
|
11
|
+
:effective_date,
|
12
|
+
:settlement_date,
|
13
|
+
:origin_status_code,
|
14
|
+
:origin_dfi_id,
|
15
|
+
:batch_number
|
16
|
+
|
17
|
+
defaults :record_type => 5,
|
18
|
+
:service_class_code => 200,
|
19
|
+
:company_note_data => '',
|
20
|
+
:date => lambda{ Time.now.strftime("%y%m%d") },
|
21
|
+
:settlement_date => '',
|
22
|
+
:origin_status_code => ''
|
23
|
+
end
|
24
|
+
end
|
data/lib/ach/batch.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
module ACH
|
2
|
+
class Batch < Component
|
3
|
+
has_many :entries
|
4
|
+
|
5
|
+
def has_credit?
|
6
|
+
entries.any?(&:credit?)
|
7
|
+
end
|
8
|
+
|
9
|
+
def has_debit?
|
10
|
+
entries.any?(&:debit?)
|
11
|
+
end
|
12
|
+
|
13
|
+
def entry_count
|
14
|
+
entries.length
|
15
|
+
end
|
16
|
+
|
17
|
+
def entry_hash
|
18
|
+
entries.map{ |e| e.routing_number.to_i / 10 }.compact.inject(&:+)
|
19
|
+
end
|
20
|
+
|
21
|
+
def total_debit_amount
|
22
|
+
entries.select(&:debit?).map{ |e| e.amount.to_i }.compact.inject(&:+) || 0
|
23
|
+
end
|
24
|
+
|
25
|
+
def total_credit_amount
|
26
|
+
entries.select(&:credit?).map{ |e| e.amount.to_i }.compact.inject(&:+) || 0
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_ach
|
30
|
+
[header] + entries + [control]
|
31
|
+
end
|
32
|
+
|
33
|
+
def before_header
|
34
|
+
attributes[:service_class_code] ||= (has_debit? && has_credit? ? 200 : has_debit? ? 225 : 220)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module ACH
|
2
|
+
class Component
|
3
|
+
include Validations
|
4
|
+
|
5
|
+
class UnknownAttribute < ArgumentError
|
6
|
+
def initialize field
|
7
|
+
super "Unrecognized attribute '#{field}'"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :attributes
|
12
|
+
|
13
|
+
def initialize fields = {}, &block
|
14
|
+
@attributes = {}
|
15
|
+
fields.each do |name, value|
|
16
|
+
raise UnknownAttribute.new(name) unless Formatter::RULES.key?(name)
|
17
|
+
@attributes[name] = value
|
18
|
+
end
|
19
|
+
after_initialize if respond_to?(:after_initialize)
|
20
|
+
instance_eval(&block) if block
|
21
|
+
end
|
22
|
+
|
23
|
+
def method_missing meth, *args
|
24
|
+
if Formatter::RULES.key?(meth)
|
25
|
+
args.empty? ? @attributes[meth] : (@attributes[meth] = args.first)
|
26
|
+
else
|
27
|
+
super
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def before_header
|
32
|
+
end
|
33
|
+
private :before_header
|
34
|
+
|
35
|
+
def header fields = {}, &block
|
36
|
+
before_header
|
37
|
+
merged_fields = fields_for(self.class::Header).merge(fields)
|
38
|
+
@header ||= self.class::Header.new(merged_fields)
|
39
|
+
@header.tap do |head|
|
40
|
+
head.instance_eval(&block) if block
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def control
|
45
|
+
klass = self.class::Control
|
46
|
+
fields = klass.fields.select{ |f| respond_to?(f) || attributes[f] }
|
47
|
+
klass.new Hash[*fields.zip(fields.map{ |f| send(f) }).flatten]
|
48
|
+
end
|
49
|
+
|
50
|
+
def fields_for component_or_class
|
51
|
+
klass = component_or_class.is_a?(Class) ? component_or_class : "ACH::#{component_or_class.camelize}".constantize
|
52
|
+
klass < Component ? attributes : attributes.select{ |k, v| klass.fields.include?(k) && attributes[k] }
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.has_many plural_name, proc_defaults = nil
|
56
|
+
attr_reader plural_name
|
57
|
+
|
58
|
+
singular_name = plural_name.to_s.singularize
|
59
|
+
klass = "ACH::#{singular_name.camelize}".constantize
|
60
|
+
|
61
|
+
define_method(singular_name) do |*args, &block|
|
62
|
+
index_or_fields = args.first || {}
|
63
|
+
return send(plural_name)[index_or_fields] if Fixnum === index_or_fields
|
64
|
+
|
65
|
+
defaults = proc_defaults ? instance_exec(&proc_defaults) : {}
|
66
|
+
|
67
|
+
klass.new(fields_for(singular_name).merge(defaults).merge(index_or_fields)).tap do |component|
|
68
|
+
component.instance_eval(&block) if block
|
69
|
+
send(plural_name) << component
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
define_method :after_initialize do
|
74
|
+
instance_variable_set("@#{plural_name}", [])
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/lib/ach/entry.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
module ACH
|
2
|
+
class Entry < Record
|
3
|
+
CREDIT_TRANSACTION_CODE_ENDING_DIGITS = ('0'..'4').to_a.freeze
|
4
|
+
|
5
|
+
fields :record_type,
|
6
|
+
:transaction_code,
|
7
|
+
:routing_number,
|
8
|
+
:bank_account,
|
9
|
+
:amount,
|
10
|
+
:customer_acct,
|
11
|
+
:customer_name,
|
12
|
+
:transaction_type,
|
13
|
+
:addenda,
|
14
|
+
:bank_15
|
15
|
+
|
16
|
+
defaults :record_type => 6,
|
17
|
+
:transaction_code => 27,
|
18
|
+
:transaction_type => 'S',
|
19
|
+
:customer_acct => '',
|
20
|
+
:addenda => 0,
|
21
|
+
:bank_15 => ''
|
22
|
+
|
23
|
+
def debit?
|
24
|
+
!credit?
|
25
|
+
end
|
26
|
+
|
27
|
+
def credit?
|
28
|
+
CREDIT_TRANSACTION_CODE_ENDING_DIGITS.include? transaction_code.to_s[1..1]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module ACH
|
2
|
+
class File::Control < Record
|
3
|
+
fields :record_type,
|
4
|
+
:batch_count,
|
5
|
+
:block_count,
|
6
|
+
:entry_count,
|
7
|
+
:entry_hash,
|
8
|
+
:total_debit_amount,
|
9
|
+
:total_credit_amount,
|
10
|
+
:bank_39
|
11
|
+
|
12
|
+
defaults :record_type => 9,
|
13
|
+
:bank_39 => ''
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module ACH
|
2
|
+
class File::Header < Record
|
3
|
+
fields :record_type,
|
4
|
+
:priority_code,
|
5
|
+
:immediate_dest,
|
6
|
+
:immediate_origin,
|
7
|
+
:date,
|
8
|
+
:time,
|
9
|
+
:file_id_modifier,
|
10
|
+
:record_size,
|
11
|
+
:blocking_factor,
|
12
|
+
:format_code,
|
13
|
+
:immediate_dest_name,
|
14
|
+
:immediate_origin_name,
|
15
|
+
:reference_code
|
16
|
+
|
17
|
+
defaults :record_type => 1,
|
18
|
+
:priority_code => 1,
|
19
|
+
:reference_code => '',
|
20
|
+
:date => lambda{ Time.now.strftime("%y%m%d") },
|
21
|
+
:time => lambda{ Time.now.strftime("%H%M") },
|
22
|
+
:file_id_modifier => 'A',
|
23
|
+
:record_size => 94,
|
24
|
+
:blocking_factor => 10,
|
25
|
+
:format_code => 1,
|
26
|
+
:reference_code => ''
|
27
|
+
end
|
28
|
+
end
|
data/lib/ach/file.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
module ACH
|
2
|
+
class File < Component
|
3
|
+
has_many :batches, lambda{ {:batch_number => batches.length + 1} }
|
4
|
+
|
5
|
+
def batch_count
|
6
|
+
batches.length
|
7
|
+
end
|
8
|
+
|
9
|
+
def block_count
|
10
|
+
(entry_count / 10.0).ceil
|
11
|
+
end
|
12
|
+
|
13
|
+
def entry_count
|
14
|
+
batches.map{ |b| b.entries.length }.inject(&:+) || 0
|
15
|
+
end
|
16
|
+
|
17
|
+
def entry_hash
|
18
|
+
batches.map(&:entry_hash).compact.inject(&:+)
|
19
|
+
end
|
20
|
+
|
21
|
+
def total_debit_amount
|
22
|
+
batches.map(&:total_debit_amount).compact.inject(&:+)
|
23
|
+
end
|
24
|
+
|
25
|
+
def total_credit_amount
|
26
|
+
batches.map(&:total_credit_amount).compact.inject(&:+)
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_ach
|
30
|
+
[header] + batches.map(&:to_ach).flatten + [control]
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_s!
|
34
|
+
to_ach.map(&:to_s!).join("\n")
|
35
|
+
end
|
36
|
+
|
37
|
+
def write filename
|
38
|
+
return false unless valid?
|
39
|
+
::File.open(filename, 'w') do |fh|
|
40
|
+
fh.write(to_s!)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module ACH
|
2
|
+
module Formatter
|
3
|
+
extend self
|
4
|
+
|
5
|
+
RULES = {
|
6
|
+
:customer_name => '<-22',
|
7
|
+
:customer_acct => '<-15',
|
8
|
+
:amount => '->10',
|
9
|
+
:bank_2 => '<-2',
|
10
|
+
:transaction_type => '<-2',
|
11
|
+
:bank_15 => '<-15',
|
12
|
+
:addenda => '<-1',
|
13
|
+
:trace_num => '<-15',
|
14
|
+
:transaction_code => '<-2',
|
15
|
+
:record_type => '<-1',
|
16
|
+
:bank_account => '<-17',
|
17
|
+
:routing_number => '->9',
|
18
|
+
:priority_code => '->2',
|
19
|
+
:immediate_dest => '->10-',
|
20
|
+
:immediate_origin => '->10-',
|
21
|
+
:date => '<-6',
|
22
|
+
:time => '<-4',
|
23
|
+
:file_id_modifier => '<-1|upcase',
|
24
|
+
:record_size => '->3',
|
25
|
+
:blocking_factor => '->20',
|
26
|
+
:format_code => '<-1',
|
27
|
+
:immediate_dest_name => '<-23',
|
28
|
+
:immediate_origin_name => '<-23',
|
29
|
+
:reference_code => '<-8',
|
30
|
+
:service_class_code => '<-3',
|
31
|
+
:company_name => '<-16',
|
32
|
+
:company_note_data => '<-20',
|
33
|
+
:company_id => '<-10',
|
34
|
+
:entry_class_code => '<-3',
|
35
|
+
:company_entry_descr => '<-10',
|
36
|
+
:effective_date => '<-6',
|
37
|
+
:settlement_date => '<-3',
|
38
|
+
:origin_status_code => '<-1',
|
39
|
+
:origin_dfi_id => '<-8',
|
40
|
+
:batch_number => '->7',
|
41
|
+
:entry_count => '->6',
|
42
|
+
:entry_hash => '->10',
|
43
|
+
:total_debit_amount => '->12',
|
44
|
+
:total_credit_amount => '->12',
|
45
|
+
:authen_code => '<-19',
|
46
|
+
:bank_6 => '<-6',
|
47
|
+
:batch_count => '->6',
|
48
|
+
:block_count => '->6',
|
49
|
+
:entry_count => '->8',
|
50
|
+
:bank_39 => '<-39'
|
51
|
+
}.freeze
|
52
|
+
|
53
|
+
RULE_PARSER_REGEX = /^(<-|->)(\d+)(-)?(\|\w+)?$/
|
54
|
+
|
55
|
+
@@compiled_rules = {}
|
56
|
+
|
57
|
+
def format field_name, value
|
58
|
+
compile_rule(field_name) unless @@compiled_rules.key?(field_name)
|
59
|
+
@@compiled_rules[field_name].call(value)
|
60
|
+
end
|
61
|
+
|
62
|
+
def method_missing meth, *args
|
63
|
+
if RULES.key? meth
|
64
|
+
format meth, *args
|
65
|
+
else
|
66
|
+
super
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def compile_rule field_name
|
71
|
+
just, width, pad, transf = RULES[field_name].match(RULE_PARSER_REGEX)[1..-1]
|
72
|
+
padmethod = just == '<-' ? :ljust : :rjust
|
73
|
+
length = width.to_i
|
74
|
+
padstr = padmethod == :ljust ? ' ' : pad == '-' ? ' ' : '0'
|
75
|
+
transform = transf[1..-1] if transf
|
76
|
+
@@compiled_rules[field_name] = lambda{ |val| val = val.to_s[0..length]; (transform ? val.send(transform) : val).send(padmethod, length, padstr) }
|
77
|
+
end
|
78
|
+
private :compile_rule
|
79
|
+
end
|
80
|
+
end
|
data/lib/ach/record.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
module ACH
|
2
|
+
class Record
|
3
|
+
include Validations
|
4
|
+
|
5
|
+
class UnknownField < ArgumentError
|
6
|
+
def initialize field, class_name
|
7
|
+
super "Unrecognized field '#{field}' in class #{class_name}"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class EmptyField < ArgumentError
|
12
|
+
def initialize field
|
13
|
+
super "Empty field '#{field}' for #{inspect}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.fields *field_names
|
18
|
+
return @fields if field_names.empty?
|
19
|
+
@fields = field_names
|
20
|
+
@fields.each do |field|
|
21
|
+
raise UnknownField.new(field, name) unless Formatter::RULES.key?(field)
|
22
|
+
|
23
|
+
define_method(field) do |*args|
|
24
|
+
args.empty? ? fields[field] : (fields[field] = args.first)
|
25
|
+
end
|
26
|
+
|
27
|
+
define_method("#{field}=") do |val|
|
28
|
+
fields[field] = val
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.defaults default_values = nil
|
34
|
+
return @defaults if default_values.nil?
|
35
|
+
@defaults = default_values.freeze
|
36
|
+
end
|
37
|
+
|
38
|
+
def initialize fields = {}, &block
|
39
|
+
defaults.each do |key, value|
|
40
|
+
self.fields[key] = Proc === value ? value.call : value
|
41
|
+
end
|
42
|
+
self.fields.merge!(fields)
|
43
|
+
instance_eval(&block) if block
|
44
|
+
end
|
45
|
+
|
46
|
+
def defaults
|
47
|
+
self.class.defaults
|
48
|
+
end
|
49
|
+
|
50
|
+
def fields
|
51
|
+
@fields ||= {}
|
52
|
+
end
|
53
|
+
|
54
|
+
def []= name, val
|
55
|
+
fields[name] = val
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_s!
|
59
|
+
self.class.fields.map do |name|
|
60
|
+
raise EmptyField.new(name) if @fields[name].nil?
|
61
|
+
Formatter.format name, @fields[name]
|
62
|
+
end.join
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module ACH
|
2
|
+
module Validations
|
3
|
+
def valid?
|
4
|
+
reset_errors!
|
5
|
+
is_a?(Component) ? valid_component? : valid_record?
|
6
|
+
errors.empty?
|
7
|
+
end
|
8
|
+
|
9
|
+
def valid_component?
|
10
|
+
counts = {}
|
11
|
+
to_ach.each do |record|
|
12
|
+
counts[record.class] ||= 0
|
13
|
+
unless record.valid?
|
14
|
+
errors["#{record.class.name}##{counts[record.class] += 1}"] = record.errors
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
private :valid_component?
|
19
|
+
|
20
|
+
def valid_record?
|
21
|
+
self.class.fields.each do |field|
|
22
|
+
errors[field] = "is required" unless fields[field]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
private :valid_record?
|
26
|
+
|
27
|
+
def errors
|
28
|
+
@errors || reset_errors!
|
29
|
+
end
|
30
|
+
|
31
|
+
def reset_errors!
|
32
|
+
@errors = ActiveSupport::OrderedHash.new
|
33
|
+
end
|
34
|
+
private :reset_errors!
|
35
|
+
end
|
36
|
+
end
|
data/lib/ach/version.rb
ADDED
data/lib/ach_builder.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'active_support/inflector'
|
2
|
+
require 'active_support/ordered_hash'
|
3
|
+
|
4
|
+
require "ach/version"
|
5
|
+
|
6
|
+
require 'ach/formatter'
|
7
|
+
require 'ach/validations'
|
8
|
+
require 'ach/component'
|
9
|
+
require 'ach/record'
|
10
|
+
require 'ach/entry'
|
11
|
+
require 'ach/batch'
|
12
|
+
require 'ach/batch/header'
|
13
|
+
require 'ach/batch/control'
|
14
|
+
require 'ach/file'
|
15
|
+
require 'ach/file/header'
|
16
|
+
require 'ach/file/control'
|
17
|
+
|
18
|
+
module ACH
|
19
|
+
def self.sample_file
|
20
|
+
File.new(:company_id => '11-11111', :company_name => 'MY COMPANY') do
|
21
|
+
immediate_dest '123123123'
|
22
|
+
immediate_dest_name 'COMMERCE BANK'
|
23
|
+
immediate_origin '123123123'
|
24
|
+
immediate_origin_name 'MYCOMPANY'
|
25
|
+
|
26
|
+
['WEB', 'TEL'].each do |code|
|
27
|
+
batch(:entry_class_code => code, :company_entry_descr => 'TV-TELCOM') do
|
28
|
+
effective_date Time.now.strftime('%y%m%d')
|
29
|
+
origin_dfi_id "00000000"
|
30
|
+
entry :customer_name => 'JOHN SMITH',
|
31
|
+
:customer_acct => '61242882282',
|
32
|
+
:amount => '2501',
|
33
|
+
:routing_number => '010010101',
|
34
|
+
:bank_account => '103030030'
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/spec/batch_spec.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ACH::Batch do
|
4
|
+
before(:each) do
|
5
|
+
@batch = ACH::Batch.new
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should create entry with attributes" do
|
9
|
+
entry = @batch.entry :amount => 100
|
10
|
+
entry.should be_instance_of(ACH::Entry)
|
11
|
+
entry.amount.should == 100
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should create entry with attributes" do
|
15
|
+
entry = @batch.entry do
|
16
|
+
amount 100
|
17
|
+
end
|
18
|
+
entry.amount.should == 100
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should return false for has_credit? and has_debit? for empty entries" do
|
22
|
+
@batch.has_credit?.should be_false
|
23
|
+
@batch.has_debit?.should be_false
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should return true for has_credit? if contains credit entry" do
|
27
|
+
@batch.entry :amount => 100, :transaction_code => 21
|
28
|
+
@batch.has_credit?.should be_true
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should return true for has_debit? if contains debit entry" do
|
32
|
+
@batch.entry :amount => 100
|
33
|
+
@batch.has_debit?.should be_true
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should generate 225 service_class_code for header if with debit entry only" do
|
37
|
+
@batch.entry :amount => 100
|
38
|
+
@batch.header.service_class_code.should == 225
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should generate 220 service_class_code for header if with credit entry only" do
|
42
|
+
@batch.entry :amount => 100, :transaction_code => 21
|
43
|
+
@batch.header.service_class_code.should == 220
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should generate 200 service_class_code for header if with debit and credit entries" do
|
47
|
+
@batch.entry :amount => 100
|
48
|
+
@batch.entry :amount => 100, :transaction_code => 21
|
49
|
+
@batch.header.service_class_code.should == 200
|
50
|
+
end
|
51
|
+
end
|
data/spec/file_spec.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ACH::File do
|
4
|
+
before(:each) do
|
5
|
+
@attributes = {
|
6
|
+
:company_id => '11-11111',
|
7
|
+
:company_name => 'MY COMPANY',
|
8
|
+
:immediate_dest => '123123123',
|
9
|
+
:immediate_dest_name => 'COMMERCE BANK',
|
10
|
+
:immediate_origin => '123123123',
|
11
|
+
:immediate_origin_name => 'MYCOMPANY' }
|
12
|
+
@invalid_attributes = {:foo => 'bar'}
|
13
|
+
@file = ACH::File.new(@attributes)
|
14
|
+
@file_with_batch = ACH::File.new(@attributes) do
|
15
|
+
batch :entry_class_code => 'WEB'
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should correctly assign attributes" do
|
20
|
+
@file.company_id.should == '11-11111'
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should be modified by calling attribute methods in block" do
|
24
|
+
file = ACH::File.new(@attributes) do
|
25
|
+
company_name "MINE COMPANY"
|
26
|
+
end
|
27
|
+
file.company_name.should == "MINE COMPANY"
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should fetch and return header" do
|
31
|
+
head = @file.header
|
32
|
+
head.should be_instance_of(ACH::File::Header)
|
33
|
+
head.immediate_dest.should == '123123123'
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should be able to modify header info in block form" do
|
37
|
+
file = ACH::File.new(@attributes) do
|
38
|
+
header(:immediate_dest => '321321321') do
|
39
|
+
immediate_dest_name 'BANK COMMERCE'
|
40
|
+
end
|
41
|
+
end
|
42
|
+
head = file.header
|
43
|
+
head.immediate_dest.should == '321321321'
|
44
|
+
head.immediate_dest_name.should == 'BANK COMMERCE'
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should raise exception on unknown attribute assignement" do
|
48
|
+
lambda {
|
49
|
+
ACH::File.new(@invalid_attributes)
|
50
|
+
}.should raise_error(ACH::Component::UnknownAttribute)
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should be able to create a batch" do
|
54
|
+
@file_with_batch.batches.should_not be_empty
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should return a batch when index is passed" do
|
58
|
+
@file_with_batch.batch(0).should be_instance_of(ACH::Batch)
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should assign a batch_number to a batch" do
|
62
|
+
batch = @file_with_batch.batch(0)
|
63
|
+
batch.batch_number.should == 1
|
64
|
+
batch = @file_with_batch.batch(:entry_class_code => 'WEB')
|
65
|
+
batch.batch_number.should == 2
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should assign attributes to a batch" do
|
69
|
+
batch = @file_with_batch.batch(0)
|
70
|
+
batch.attributes.should include(@file_with_batch.attributes)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ACH::Formatter do
|
4
|
+
before(:all) do
|
5
|
+
module ACH::Formatter
|
6
|
+
# redefining RULES FOR new test values
|
7
|
+
RULES = RULES.dup
|
8
|
+
RULES[:ljust_10] = '<-10'
|
9
|
+
RULES[:ljust_10_transform] = '<-10|upcase'
|
10
|
+
RULES[:rjust_10] = '->10'
|
11
|
+
RULES[:rjust_10_spaced] = '->10-'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
it{ ACH::Formatter.ljust_10('FOO').should == 'FOO'.ljust(10) }
|
16
|
+
|
17
|
+
it{ ACH::Formatter.ljust_10_transform('foo').should == 'FOO'.ljust(10) }
|
18
|
+
|
19
|
+
it{ ACH::Formatter.rjust_10(1599).should == '1599'.rjust(10, '0') }
|
20
|
+
|
21
|
+
it{ ACH::Formatter.rjust_10_spaced(1599).should == '1599'.rjust(10) }
|
22
|
+
end
|
data/spec/record_spec.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ACH::Record do
|
4
|
+
before(:all) do
|
5
|
+
class Entry < ACH::Record
|
6
|
+
fields :customer_name, :amount
|
7
|
+
defaults :customer_name => 'JOHN SMITH'
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should have 2 ordered fields" do
|
12
|
+
Entry.fields.should == [:customer_name, :amount]
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should create a record with default value" do
|
16
|
+
Entry.new.customer_name.should == 'JOHN SMITH'
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should overwrite default value" do
|
20
|
+
entry = Entry.new(:customer_name => 'SMITH JOHN')
|
21
|
+
entry.customer_name.should == 'SMITH JOHN'
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should generate formatted string" do
|
25
|
+
entry = Entry.new :amount => 1599
|
26
|
+
entry.to_s!.should == "JOHN SMITH".ljust(22) + "1599".rjust(10, '0')
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should raise exception with unfilled value" do
|
30
|
+
lambda{
|
31
|
+
Entry.new.to_s!
|
32
|
+
}.should raise_error(ACH::Record::EmptyField)
|
33
|
+
end
|
34
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ach_builder
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Artem Kuzko
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-09-14 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: &81171620 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 2.0.0
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *81171620
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: active_support
|
27
|
+
requirement: &81171160 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 2.3.0
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *81171160
|
36
|
+
description: Ruby tools for building ACH (Automated Clearing House) files
|
37
|
+
email:
|
38
|
+
- a.kuzko@gmail.com
|
39
|
+
executables: []
|
40
|
+
extensions: []
|
41
|
+
extra_rdoc_files: []
|
42
|
+
files:
|
43
|
+
- .gitignore
|
44
|
+
- .rspec
|
45
|
+
- Gemfile
|
46
|
+
- MIT-LICENSE
|
47
|
+
- README.md
|
48
|
+
- Rakefile
|
49
|
+
- ach_builder.gemspec
|
50
|
+
- lib/ach/batch.rb
|
51
|
+
- lib/ach/batch/control.rb
|
52
|
+
- lib/ach/batch/header.rb
|
53
|
+
- lib/ach/component.rb
|
54
|
+
- lib/ach/entry.rb
|
55
|
+
- lib/ach/file.rb
|
56
|
+
- lib/ach/file/control.rb
|
57
|
+
- lib/ach/file/header.rb
|
58
|
+
- lib/ach/formatter.rb
|
59
|
+
- lib/ach/record.rb
|
60
|
+
- lib/ach/validations.rb
|
61
|
+
- lib/ach/version.rb
|
62
|
+
- lib/ach_builder.rb
|
63
|
+
- spec/batch_spec.rb
|
64
|
+
- spec/file_spec.rb
|
65
|
+
- spec/formatter_spec.rb
|
66
|
+
- spec/record_spec.rb
|
67
|
+
- spec/spec_helper.rb
|
68
|
+
homepage: http://github.com/akuzko/ach_builder
|
69
|
+
licenses: []
|
70
|
+
post_install_message:
|
71
|
+
rdoc_options: []
|
72
|
+
require_paths:
|
73
|
+
- lib
|
74
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
75
|
+
none: false
|
76
|
+
requirements:
|
77
|
+
- - ! '>='
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '0'
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
requirements: []
|
87
|
+
rubyforge_project:
|
88
|
+
rubygems_version: 1.8.6
|
89
|
+
signing_key:
|
90
|
+
specification_version: 3
|
91
|
+
summary: Ruby tools for building ACH files
|
92
|
+
test_files: []
|