ach_builder 0.0.1.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.
- 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: []
|