ach_builder 0.2.1 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/Gemfile +1 -1
- data/ach_builder.gemspec +1 -1
- data/lib/ach/batch/builder.rb +27 -8
- data/lib/ach/component.rb +57 -12
- data/lib/ach/component/has_many_association.rb +57 -19
- data/lib/ach/constants.rb +8 -1
- data/lib/ach/file.rb +5 -2
- data/lib/ach/file/builder.rb +45 -16
- data/lib/ach/file/reader.rb +31 -0
- data/lib/ach/file/transmission_header.rb +19 -5
- data/lib/ach/formatter.rb +33 -11
- data/lib/ach/formatter/rule.rb +6 -3
- data/lib/ach/record/base.rb +54 -14
- data/lib/ach/record/dynamic.rb +18 -2
- data/lib/ach/record/entry.rb +12 -4
- data/lib/ach/validations.rb +23 -9
- data/lib/ach/version.rb +2 -1
- data/lib/ach_builder.rb +5 -0
- data/tmp/metric_fu/_data/20111221.yml +7231 -0
- metadata +22 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 13b50247a53e8f32fca29c49375e6d700bd039bf
|
4
|
+
data.tar.gz: 84d5a95e03d5f96f1a591c47d23e748cc613c1ca
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6442a48f3ac4122b01339c4f95d5b4306efe21437834633211ca5bb16495cb2e07a8a08e59f8a28406a898c80101fdd05e3c8269152bb8d679876dd79c6974b4
|
7
|
+
data.tar.gz: 65e1c78ea5211a84b37394cfa84c2ef296cc6248291139f26d7a72bebd1c152189d3a1bce1903c5f950869832b3785a01a1adc16050594c79d08dcbef8c4d62e
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.0.0
|
1
|
+
2.0.0
|
data/Gemfile
CHANGED
@@ -4,10 +4,10 @@ source "https://rubygems.org"
|
|
4
4
|
# Specify your gem's dependencies in ach_builder.gemspec
|
5
5
|
gem 'i18n'
|
6
6
|
gemspec
|
7
|
-
gem 'rspec'
|
8
7
|
|
9
8
|
group :development, :test do
|
10
9
|
gem 'rspec'
|
10
|
+
gem 'yard'
|
11
11
|
|
12
12
|
gem "simplecov" , :require => false
|
13
13
|
gem 'simplecov-rcov-text', :require => false
|
data/ach_builder.gemspec
CHANGED
@@ -6,7 +6,7 @@ Gem::Specification.new do |s|
|
|
6
6
|
s.name = "ach_builder"
|
7
7
|
s.version = ACH::VERSION
|
8
8
|
s.authors = ["TMX Credit", "Artem Kuzko", "Sergey Potapov"]
|
9
|
-
s.email = ["rubygems@tmxcredit.com", "AKuzko@sphereconsultinginc.com", "
|
9
|
+
s.email = ["rubygems@tmxcredit.com", "AKuzko@sphereconsultinginc.com", "blake131313@gmail.com"]
|
10
10
|
s.homepage = "https://github.com/TMXCredit/ach_builder"
|
11
11
|
s.summary = "Ruby tools for building ACH files"
|
12
12
|
s.description = "Ruby tools for building ACH (Automated Clearing House) files"
|
data/lib/ach/batch/builder.rb
CHANGED
@@ -2,49 +2,66 @@ module ACH
|
|
2
2
|
# Supports building a string representation of a particular instance of an
|
3
3
|
# ACH batch. Supports building ACH lines. Included by ACH::File.
|
4
4
|
module Batch::Builder
|
5
|
-
# Returns +true+ if any of internal ACH entries has 'credit' transaction code
|
5
|
+
# Returns +true+ if any of internal ACH entries has 'credit' transaction code.
|
6
|
+
#
|
7
|
+
# @return [Boolean]
|
6
8
|
def has_credit?
|
7
9
|
entries.any?(&:credit?)
|
8
10
|
end
|
9
11
|
|
10
|
-
# Returns +true+ if any internal ACH entry has 'debit' transaction code
|
12
|
+
# Returns +true+ if any internal ACH entry has 'debit' transaction code.
|
13
|
+
#
|
14
|
+
# @return [Boolean]
|
11
15
|
def has_debit?
|
12
16
|
entries.any?(&:debit?)
|
13
17
|
end
|
14
18
|
|
15
|
-
# Returns total amount of entry and addenda records within batch
|
19
|
+
# Returns total amount of entry and addenda records within batch.
|
20
|
+
#
|
21
|
+
# @return [Fixnum]
|
16
22
|
def entry_addenda_count
|
17
23
|
entries.size + addendas.values.flatten.size
|
18
24
|
end
|
19
25
|
|
20
26
|
# Returns 'hashed' representation of all entries within batch. See NACHA
|
21
|
-
# documentation for more details on entry hash
|
27
|
+
# documentation for more details on entry hash.
|
28
|
+
#
|
29
|
+
# @return [Fixnum]
|
22
30
|
def entry_hash
|
23
31
|
entries.map{ |entry| entry.routing_number.to_i / 10 }.compact.inject(&:+)
|
24
32
|
end
|
25
33
|
|
26
|
-
# Returns total amount of all 'debit' entries within a batch
|
34
|
+
# Returns total amount of all 'debit' entries within a batch.
|
35
|
+
#
|
36
|
+
# @return [Fixnum]
|
27
37
|
def total_debit_amount
|
28
38
|
amount_sum_for(:debit?)
|
29
39
|
end
|
30
40
|
|
31
|
-
# Returns total amount of all 'credit' entries within a batch
|
41
|
+
# Returns total amount of all 'credit' entries within a batch.
|
42
|
+
#
|
43
|
+
# @return [Fixnum]
|
32
44
|
def total_credit_amount
|
33
45
|
amount_sum_for(:credit?)
|
34
46
|
end
|
35
47
|
|
36
|
-
# Returns ACH record objects that represent the batch
|
48
|
+
# Returns ACH record objects that represent the batch.
|
49
|
+
#
|
50
|
+
# @return [Array<ACH::Record::Base>]
|
37
51
|
def to_ach
|
38
52
|
[header] + fetch_entries + [control]
|
39
53
|
end
|
40
54
|
|
41
|
-
# Helper method executed just before building a header record for the batch
|
55
|
+
# Helper method executed just before building a header record for the batch.
|
42
56
|
def before_header
|
43
57
|
attributes[:service_class_code] ||= (has_debit? && has_credit? ? 200 : has_debit? ? 225 : 220)
|
44
58
|
end
|
45
59
|
private :before_header
|
46
60
|
|
47
61
|
# Helper method, returns total amount of all entries within a batch, filtered by +meth+
|
62
|
+
#
|
63
|
+
# @param [Symbol] meth
|
64
|
+
# @return [Fixnum]
|
48
65
|
def amount_sum_for(meth)
|
49
66
|
entries.select(&meth).map{ |entry| entry.amount.to_i }.compact.inject(&:+) || 0
|
50
67
|
end
|
@@ -52,6 +69,8 @@ module ACH
|
|
52
69
|
|
53
70
|
# Fetches all internal records (entries and addendas) in right order, i.e. addenda records
|
54
71
|
# should be positioned right after corresponding entry records.
|
72
|
+
#
|
73
|
+
# @return [Array<ACH::Record::Base>]
|
55
74
|
def fetch_entries
|
56
75
|
entries.inject([]){ |all, entry| all << entry << addendas[entry] }.flatten.compact
|
57
76
|
end
|
data/lib/ach/component.rb
CHANGED
@@ -19,7 +19,11 @@ module ACH
|
|
19
19
|
|
20
20
|
# Exception raised on attempt to assign a value to nonexistent field.
|
21
21
|
class UnknownAttributeError < ArgumentError
|
22
|
-
|
22
|
+
# Initialize error with field and component.
|
23
|
+
#
|
24
|
+
# @param [String] field
|
25
|
+
# @param [ACH::Component] obj
|
26
|
+
def initialize(field, obj)
|
23
27
|
super "Unrecognized attribute '#{field}' for #{obj}"
|
24
28
|
end
|
25
29
|
end
|
@@ -31,6 +35,9 @@ module ACH
|
|
31
35
|
|
32
36
|
attr_reader :attributes
|
33
37
|
|
38
|
+
# When inherited, clone class-related properties.
|
39
|
+
#
|
40
|
+
# @param [Class] klass
|
34
41
|
def self.inherited(klass)
|
35
42
|
klass.default_attributes = default_attributes.dup
|
36
43
|
klass.after_initialize_hooks = after_initialize_hooks.dup
|
@@ -46,6 +53,9 @@ module ACH
|
|
46
53
|
# header record.
|
47
54
|
#
|
48
55
|
# Note that default values may be overwritten when building records.
|
56
|
+
#
|
57
|
+
# @param [Symbol] meth
|
58
|
+
# @param [*Object] args
|
49
59
|
def self.method_missing(meth, *args)
|
50
60
|
if Formatter.defined?(meth)
|
51
61
|
default_attributes[meth] = args.first
|
@@ -54,16 +64,28 @@ module ACH
|
|
54
64
|
end
|
55
65
|
end
|
56
66
|
|
57
|
-
|
58
|
-
|
67
|
+
# Initialize component with field values. If block is given, it is evaluated in context
|
68
|
+
# of component, allowing setting fields via method calls and declarations of nested
|
69
|
+
# components.
|
70
|
+
#
|
71
|
+
# @param [Hash] fields
|
72
|
+
# @raise [UnknownAttributeError]
|
73
|
+
def initialize(fields = {})
|
74
|
+
@attributes = self.class.default_attributes.dup
|
59
75
|
fields.each do |name, value|
|
60
76
|
raise UnknownAttributeError.new(name, self) unless Formatter.defined?(name)
|
61
77
|
@attributes[name] = value
|
62
78
|
end
|
63
79
|
after_initialize
|
64
|
-
instance_eval(&
|
80
|
+
instance_eval(&Proc.new) if block_given?
|
65
81
|
end
|
66
82
|
|
83
|
+
# Missing messages are treated as accessor methods for a component if their
|
84
|
+
# message name is defined by {ACH::Formatter}.
|
85
|
+
#
|
86
|
+
# @param [Symbol] meth
|
87
|
+
# @param [*Object] args
|
88
|
+
# @return [String]
|
67
89
|
def method_missing(meth, *args)
|
68
90
|
if Formatter.defined?(meth)
|
69
91
|
args.empty? ? @attributes[meth] : (@attributes[meth] = args.first)
|
@@ -72,7 +94,9 @@ module ACH
|
|
72
94
|
end
|
73
95
|
end
|
74
96
|
|
75
|
-
|
97
|
+
# Hook that is called whenever component header record is created. To be
|
98
|
+
# overridden in subclasses.
|
99
|
+
def before_header
|
76
100
|
end
|
77
101
|
private :before_header
|
78
102
|
|
@@ -92,19 +116,26 @@ module ACH
|
|
92
116
|
# == Example 3
|
93
117
|
#
|
94
118
|
# header # => just returns a header object
|
95
|
-
def header(fields = {}
|
119
|
+
def header(fields = {})
|
96
120
|
before_header
|
97
121
|
merged_fields = fields_for(self.class::Header).merge(fields)
|
98
122
|
@header ||= self.class::Header.new(merged_fields)
|
99
123
|
@header.tap do |head|
|
100
|
-
head.instance_eval(&
|
124
|
+
head.instance_eval(&Proc.new) if block_given?
|
101
125
|
end
|
102
126
|
end
|
103
127
|
|
104
|
-
|
128
|
+
# Build a component (+File+ or +Batch+) related +Header+ record from a given string.
|
129
|
+
#
|
130
|
+
# @param [String] str
|
131
|
+
# @return [ACH::Record::Base]
|
132
|
+
def build_header(str)
|
105
133
|
@header = self.class::Header.from_s(str)
|
106
134
|
end
|
107
135
|
|
136
|
+
# Build a component-related +Control+ record.
|
137
|
+
#
|
138
|
+
# @return [ACH::Record::Base]
|
108
139
|
def control
|
109
140
|
@control ||= begin
|
110
141
|
klass = self.class::Control
|
@@ -113,10 +144,19 @@ module ACH
|
|
113
144
|
end
|
114
145
|
end
|
115
146
|
|
116
|
-
|
147
|
+
# Build a component-related +Control+ record from a given string.
|
148
|
+
#
|
149
|
+
# @param [String] str
|
150
|
+
# @return [ACH::Record::Base]
|
151
|
+
def build_control(str)
|
117
152
|
@control = self.class::Control.from_s(str)
|
118
153
|
end
|
119
|
-
|
154
|
+
|
155
|
+
# Return a set of fields, that is a subset of +attributes+ that can be used to
|
156
|
+
# initialize an instance of a +klass+. +Component+ uses +attributes+ itself.
|
157
|
+
#
|
158
|
+
# @param [Class] klass
|
159
|
+
# @return [Hash]
|
120
160
|
def fields_for(klass)
|
121
161
|
if klass < Component
|
122
162
|
attributes
|
@@ -126,11 +166,13 @@ module ACH
|
|
126
166
|
end
|
127
167
|
end
|
128
168
|
|
129
|
-
|
169
|
+
# Execute all +Proc+ objects contained in the +after_initialize_hooks+
|
170
|
+
# array in the context of the object.
|
171
|
+
def after_initialize
|
130
172
|
self.class.after_initialize_hooks.each{ |hook| instance_exec(&hook) }
|
131
173
|
end
|
132
174
|
|
133
|
-
# Creates
|
175
|
+
# Creates a has_many association.
|
134
176
|
#
|
135
177
|
# == Example
|
136
178
|
#
|
@@ -147,6 +189,9 @@ module ACH
|
|
147
189
|
# The example above extends File with #batches and #batch instance methods:
|
148
190
|
# * #batch is used to add new instance of Batch.
|
149
191
|
# * #batches is used to get an array of batches which belong to file.
|
192
|
+
#
|
193
|
+
# @param [Symbol] plural_name
|
194
|
+
# @param [Hash] options
|
150
195
|
def self.has_many(plural_name, options = {})
|
151
196
|
association = HasManyAssociation.new(plural_name, options)
|
152
197
|
|
@@ -25,6 +25,10 @@ module ACH
|
|
25
25
|
# created after Entry records. Each new Addenda record will be attached
|
26
26
|
# to the latest Entry record.
|
27
27
|
class NoLinkError < ArgumentError
|
28
|
+
# Initialize the error with a descriptive message.
|
29
|
+
#
|
30
|
+
# @param [String] link
|
31
|
+
# @param [Class] klass
|
28
32
|
def initialize(link, klass)
|
29
33
|
super "No #{link} was found to attach a new #{klass}"
|
30
34
|
end
|
@@ -33,6 +37,10 @@ module ACH
|
|
33
37
|
# Exception thrown if an association object, assigned for particular
|
34
38
|
# owner object, is used to assign to another owner object
|
35
39
|
class DoubleAssignmentError < StandardError
|
40
|
+
# Initialize the error with a descriptive message.
|
41
|
+
#
|
42
|
+
# @param [String] name
|
43
|
+
# @param [ACH::Component] owner
|
36
44
|
def initialize(name, owner)
|
37
45
|
super "Association #{name} has alredy been assigned to #{owner}"
|
38
46
|
end
|
@@ -41,14 +49,23 @@ module ACH
|
|
41
49
|
attr_reader :name, :linked_to, :proc_defaults
|
42
50
|
private :linked_to, :proc_defaults
|
43
51
|
|
52
|
+
# Initialize the association with a plural name and options.
|
53
|
+
#
|
54
|
+
# @param [String] plural_name
|
55
|
+
# @param [Hash] options
|
56
|
+
# @option options [Symbol] :linked_to plural name of records to link associated ones
|
57
|
+
# @option options [Proc] :proc_defaults
|
44
58
|
def initialize(plural_name, options = {})
|
45
59
|
@name = plural_name.to_s
|
46
60
|
@linked_to, @proc_defaults = options.values_at(:linked_to, :proc_defaults)
|
47
61
|
end
|
48
62
|
|
49
|
-
#
|
63
|
+
# Clone +self+ and assign +owner+ to clone. Also, for newly created
|
50
64
|
# clone association that has owner, aliases main methods so that +owner+
|
51
65
|
# may delegate to them.
|
66
|
+
#
|
67
|
+
# @param [ACH::Component] owner
|
68
|
+
# @raise [DoubleAssignmentError]
|
52
69
|
def for(owner)
|
53
70
|
raise DoubleAssignmentError.new(@name, @owner) if @owner
|
54
71
|
|
@@ -63,51 +80,63 @@ module ACH
|
|
63
80
|
end
|
64
81
|
end
|
65
82
|
|
66
|
-
#
|
83
|
+
# Return an array of methods to be delegated by +owner+ of the association.
|
67
84
|
# For example, for association named :items, it will include:
|
68
85
|
# * +build_item+ - for instantiating Item from the string (used by parsing functionality)
|
69
86
|
# * +item+ - for instantiating Item during common ACH File creation
|
70
|
-
# * +items+ - that returns set of Item objects
|
87
|
+
# * +items+ - that returns set of Item objects.
|
88
|
+
#
|
89
|
+
# @return [Array<String>]
|
71
90
|
def delegation_methods
|
72
91
|
["build_#{singular_name}", singular_name, name]
|
73
92
|
end
|
74
93
|
|
75
|
-
#
|
94
|
+
# Use <tt>klass#from_s</tt> to instantiate object from a string. Thus, +klass+ should be
|
76
95
|
# descendant of ACH::Record::Base. Then pushes object to appropriate container.
|
96
|
+
#
|
97
|
+
# @param [String] str
|
98
|
+
# @return [Array<ACH::Record::Base>]
|
77
99
|
def build(str)
|
78
100
|
obj = klass.from_s(str)
|
79
101
|
container_for_associated << obj
|
80
102
|
end
|
81
103
|
|
82
|
-
#
|
83
|
-
# appropriate container. For example, for :items association, this method is
|
104
|
+
# Create an associated object using common to ACH controls pattern, and push it to
|
105
|
+
# an appropriate container. For example, for :items association, this method is
|
84
106
|
# aliased to +item+, so you will have:
|
107
|
+
#
|
85
108
|
# item(:code => 'WEB') do
|
86
109
|
# other_code 'BEW'
|
87
110
|
# # ...
|
88
111
|
# end
|
89
|
-
|
112
|
+
#
|
113
|
+
# @param [*Object] args
|
114
|
+
# @return [Object] instance of a class under ACH namespace
|
115
|
+
def create(*args)
|
90
116
|
fields = args.first || {}
|
91
117
|
|
92
118
|
defaults = proc_defaults ? @owner.instance_exec(&proc_defaults) : {}
|
93
119
|
|
94
120
|
klass.new(@owner.fields_for(klass).merge(defaults).merge(fields)).tap do |component|
|
95
|
-
component.instance_eval(&
|
121
|
+
component.instance_eval(&Proc.new) if block_given?
|
96
122
|
container_for_associated << component
|
97
123
|
end
|
98
124
|
end
|
99
125
|
|
100
|
-
#
|
126
|
+
# Return the main container for association. For plain (without :linked_to option), it is
|
101
127
|
# array. For linked associations, it is a hash, which keys are records from linking
|
102
|
-
# associations, and values are arrays for association's objects
|
128
|
+
# associations, and values are arrays for association's objects.
|
129
|
+
#
|
130
|
+
# @return [Hash, Array]
|
103
131
|
def container
|
104
132
|
@container ||= linked? ? {} : []
|
105
133
|
end
|
106
134
|
|
107
|
-
#
|
108
|
-
# equivalent to +container+. For linked
|
109
|
-
# association's name to get the
|
110
|
-
#
|
135
|
+
# Return an array onto which the associated object may be be pushed. For
|
136
|
+
# plain associations, it is equivalent to +container+. For linked
|
137
|
+
# associations, uses +@owner+ and linking association's name to get the
|
138
|
+
# latest record from linking associations. If it does not exist,
|
139
|
+
# +NoLinkError+ will be raised.
|
111
140
|
#
|
112
141
|
# Example:
|
113
142
|
# class Batch < ACH::Component
|
@@ -124,6 +153,9 @@ module ACH
|
|
124
153
|
# batch.entries # => [<Entry, amount=100>, <Entry, amount=200>]
|
125
154
|
# batch.addendas # => {<Entry, amount=100> => [<Addenda, text='Foo'>],
|
126
155
|
# # <Entry, amount=200> => [<Addenda, text='Bar'>, <Addenda, text='Baz'>]}
|
156
|
+
#
|
157
|
+
# @return [Array]
|
158
|
+
# @raise [NoLinkError]
|
127
159
|
def container_for_associated
|
128
160
|
return container unless linked?
|
129
161
|
|
@@ -132,21 +164,27 @@ module ACH
|
|
132
164
|
container[last_link] ||= []
|
133
165
|
end
|
134
166
|
|
135
|
-
#
|
136
|
-
# be preceded by other association's records). Returns +false+ otherwise
|
167
|
+
# Return +true+ if the association is linked to another association (thus, its records must
|
168
|
+
# be preceded by other association's records). Returns +false+ otherwise.
|
169
|
+
#
|
170
|
+
# @return [Boolean]
|
137
171
|
def linked?
|
138
172
|
!!linked_to
|
139
173
|
end
|
140
174
|
private :linked?
|
141
175
|
|
142
|
-
#
|
143
|
-
# ACH module, or in ACH::Record module
|
176
|
+
# Return +klass+ that corresponds to the association name. Should be defined either in
|
177
|
+
# ACH module, or in ACH::Record module.
|
178
|
+
#
|
179
|
+
# @return [Class]
|
144
180
|
def klass
|
145
181
|
@klass ||= ACH.to_const(@name.classify.to_sym)
|
146
182
|
end
|
147
183
|
private :klass
|
148
184
|
|
149
|
-
#
|
185
|
+
# Return the singular name of the association.
|
186
|
+
#
|
187
|
+
# @return [String]
|
150
188
|
def singular_name
|
151
189
|
@singular_name ||= name.singularize
|
152
190
|
end
|
data/lib/ach/constants.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
module ACH
|
2
|
+
# This module is a simple namespace for constants used in ACH routines.
|
2
3
|
module Constants
|
3
4
|
# The length of each record in characters.
|
4
5
|
RECORD_SIZE = 94
|
@@ -11,11 +12,17 @@ module ACH
|
|
11
12
|
# This character must be used to delimit each row.
|
12
13
|
ROWS_DELIMITER = "\n"
|
13
14
|
|
15
|
+
# ACH defined value of File Header record.
|
14
16
|
FILE_HEADER_RECORD_TYPE = 1
|
17
|
+
# ACH defined value of File Control record.
|
15
18
|
FILE_CONTROL_RECORD_TYPE = 9
|
19
|
+
# ACH defined value of Batch Header record.
|
16
20
|
BATCH_HEADER_RECORD_TYPE = 5
|
21
|
+
# ACH defined value of Batch Entry record.
|
17
22
|
BATCH_ENTRY_RECORD_TYPE = 6
|
23
|
+
# ACH defined value of Batch Addenda record.
|
18
24
|
BATCH_ADDENDA_RECORD_TYPE = 7
|
25
|
+
# ACH defined value of Batch Control record.
|
19
26
|
BATCH_CONTROL_RECORD_TYPE = 8
|
20
27
|
end
|
21
|
-
end
|
28
|
+
end
|