ach_builder 0.2.1 → 0.2.2
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 +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
|