nacha 0.1.1 → 0.1.3
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/.gitignore +3 -1
- data/Guardfile +3 -1
- data/README.md +22 -0
- data/exe/nacha +73 -0
- data/lib/nacha/aba_number.rb +23 -1
- data/lib/nacha/field.rb +31 -6
- data/lib/nacha/has_errors.rb +12 -0
- data/lib/nacha/numeric.rb +8 -5
- data/lib/nacha/parser.rb +18 -9
- data/lib/nacha/parser_context.rb +33 -0
- data/lib/nacha/record/base.rb +199 -58
- data/lib/nacha/record/filler.rb +3 -0
- data/lib/nacha/record/filler_record_type.rb +1 -1
- data/lib/nacha/record/validations/field_validations.rb +14 -4
- data/lib/nacha/version.rb +1 -1
- data/lib/nacha.rb +1 -1
- data/nacha.gemspec +3 -0
- data/templates/html_postamble.html +1 -0
- data/templates/html_preamble.html +88 -0
- metadata +52 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0f9c3cffdb4484e108a60fb3f34a59ddff273b1d519fc7e5da6dabbe3a56c49a
|
4
|
+
data.tar.gz: 8e2ec6f4a3a18b795e4cd9906942262766439bab0b6f309a95f57f7fe7931676
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ac4490619d1bf8be9ea04b8ee4544e0498a2c59921a4a6c374dfa3d659ea17f4b03d03947f84ad630859511358da9e1f3a4e5992d53106a9285f903b8b5ea791
|
7
|
+
data.tar.gz: ef3dc1c6be90957f1fc056fc532235e3b8841ab10a259e507b1aebacd784997db57f65630a703bbbd06d57b067a670697045eca088e0c268775ad43953a4f8cb
|
data/.gitignore
CHANGED
data/Guardfile
CHANGED
@@ -12,7 +12,9 @@ guard :rspec, cmd: 'bundle exec rspec' do
|
|
12
12
|
watch(rspec.spec_support) { rspec.spec_dir }
|
13
13
|
watch(rspec.spec_files)
|
14
14
|
watch(%r{^lib/nacha/base_record\.rb}) { Dir.glob('spec/nacha/record_types/*_spec.rb') }
|
15
|
-
watch(%r{^lib/config/definitions/(.+)\.yml})
|
15
|
+
watch(%r{^lib/config/definitions/(.+)\.yml}) do |m|
|
16
|
+
"spec/nacha/record_types/#{m[1]}_spec.rb"
|
17
|
+
end
|
16
18
|
|
17
19
|
# Ruby files
|
18
20
|
ruby = dsl.ruby
|
data/README.md
CHANGED
@@ -56,6 +56,28 @@ API may change at any time. Pull requests welcomed
|
|
56
56
|
}
|
57
57
|
```
|
58
58
|
|
59
|
+
## Parse an ach file into an HTML file
|
60
|
+
|
61
|
+
```
|
62
|
+
nacha parse ach_file.ach > ach_file.html`
|
63
|
+
|
64
|
+
|
65
|
+
## Discussion
|
66
|
+
|
67
|
+
* Nacha::Record::Base defines a class method `nacha_field`
|
68
|
+
* Each ACH record class defines its fields using `nacha_field`
|
69
|
+
* Based on the information provided by `nacha_field` a regex matcher
|
70
|
+
for different record types can be built out of the constant parts
|
71
|
+
of the ACH record.
|
72
|
+
* Each ACH record has a "RecordType" mixin that specifies the record
|
73
|
+
types that can follow this record.
|
74
|
+
|
75
|
+
Parsing starts by looking for a default record type 'Nacha::Record::FileHeader'
|
76
|
+
When that is found, the valid child record types for 'Nacha::Record::FileHeader'
|
77
|
+
are gathered and the subsequent lines are parsed using only those types
|
78
|
+
|
79
|
+
When a record is created, the fields for the instance are created from
|
80
|
+
the field definitions.
|
59
81
|
|
60
82
|
## Development
|
61
83
|
|
data/exe/nacha
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'thor'
|
5
|
+
require 'nacha' # Assuming this loads the Nacha gem
|
6
|
+
|
7
|
+
module Nacha
|
8
|
+
class CLI < Thor
|
9
|
+
TEMPLATES_DIR = File.join(Gem::Specification.find_by_name("nacha").gem_dir,
|
10
|
+
"templates").freeze
|
11
|
+
|
12
|
+
HTML_PREAMBLE_FILE = File.join(TEMPLATES_DIR, "html_preamble.html")
|
13
|
+
HTML_POSTAMBLE_FILE = File.join(TEMPLATES_DIR, "html_postamble.html")
|
14
|
+
|
15
|
+
desc "parse FILE", "Parse an ACH file"
|
16
|
+
def parse(file_path)
|
17
|
+
unless File.exist?(file_path)
|
18
|
+
puts "Error: File not found at #{file_path}"
|
19
|
+
exit 1
|
20
|
+
end
|
21
|
+
|
22
|
+
ach_file = [Nacha.parse(File.open(file_path)).first] # Use Nacha.parse
|
23
|
+
|
24
|
+
# TODO: Determine a user-friendly way to output the parsed data.
|
25
|
+
# For now, let's print the records.
|
26
|
+
if ach_file && ach_file.is_a?(Array) && !ach_file.empty?
|
27
|
+
output_html(file_path, ach_file)
|
28
|
+
else
|
29
|
+
puts "Could not parse the file or the file was empty."
|
30
|
+
end
|
31
|
+
rescue StandardError => e
|
32
|
+
puts "An error occurred during parsing: #{e.message}"
|
33
|
+
puts e.backtrace.join("\n")
|
34
|
+
exit 1
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def output_html(file_path, ach_file)
|
40
|
+
puts html_preamble
|
41
|
+
puts "<h1>Successfully parsed #{file_path}</h1>\n"
|
42
|
+
display_child(0, ach_file.first) # Display the first record
|
43
|
+
puts html_postamble
|
44
|
+
end
|
45
|
+
|
46
|
+
def html_preamble
|
47
|
+
@html_preamble ||= File.read(HTML_PREAMBLE_FILE)
|
48
|
+
end
|
49
|
+
|
50
|
+
def html_postamble
|
51
|
+
@html_postamble ||= File.read(HTML_POSTAMBLE_FILE)
|
52
|
+
end
|
53
|
+
|
54
|
+
def display_child(level, record)
|
55
|
+
# Attempt to call a summary or to_s method if it exists,
|
56
|
+
# otherwise inspect the record.
|
57
|
+
return unless record
|
58
|
+
level_indent = ' ' * level.to_i
|
59
|
+
puts "<html>"
|
60
|
+
puts record.to_html
|
61
|
+
if record.respond_to?(:children) && record.children.any?
|
62
|
+
if record.children.any?
|
63
|
+
record.children.each_with_index do |child_record, child_index|
|
64
|
+
display_child(level + 1, child_record)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
puts "</html>"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
Nacha::CLI.start(ARGV)
|
data/lib/nacha/aba_number.rb
CHANGED
@@ -1,9 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "nacha/has_errors"
|
4
|
+
|
3
5
|
class Nacha::AbaNumber
|
4
6
|
attr_reader :routing_number
|
5
7
|
attr_reader :aba_number
|
6
8
|
|
9
|
+
include HasErrors
|
10
|
+
|
7
11
|
def initialize(routing_number)
|
8
12
|
self.routing_number = routing_number
|
9
13
|
end
|
@@ -31,7 +35,25 @@ class Nacha::AbaNumber
|
|
31
35
|
end
|
32
36
|
|
33
37
|
def valid?
|
34
|
-
@valid ||=
|
38
|
+
@valid ||= valid_routing_number_length? && valid_check_digit?
|
39
|
+
end
|
40
|
+
|
41
|
+
def valid_routing_number_length?
|
42
|
+
if @routing_number.length != 9
|
43
|
+
add_error("Routing number must be 9 digits long, but was #{@routing_number.length} digits long.")
|
44
|
+
false
|
45
|
+
else
|
46
|
+
true
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def valid_check_digit?
|
51
|
+
if compute_check_digit != @routing_number.chars[8]
|
52
|
+
add_error("Incorrect Check Digit \"#{@routing_number.chars[8]}\" should be \"#{ compute_check_digit }\" ")
|
53
|
+
false
|
54
|
+
else
|
55
|
+
true
|
56
|
+
end
|
35
57
|
end
|
36
58
|
|
37
59
|
def to_s(with_checkdigit = true)
|
data/lib/nacha/field.rb
CHANGED
@@ -40,6 +40,13 @@ class Nacha::Field
|
|
40
40
|
case @contents
|
41
41
|
when /\AC(.*)\z/ # Constant
|
42
42
|
@data = Regexp.last_match(1)
|
43
|
+
when /\$.*¢*/
|
44
|
+
@data_type = Nacha::Numeric
|
45
|
+
@justification = :rjust
|
46
|
+
cents = 10 ** (@contents.count('¢'))
|
47
|
+
@json_output = [[:to_i], [:/, cents]]
|
48
|
+
@output_conversion = [:to_i]
|
49
|
+
@fill_character = '0'
|
43
50
|
when /Numeric/
|
44
51
|
@data_type = Nacha::Numeric
|
45
52
|
@justification = :rjust
|
@@ -64,18 +71,17 @@ class Nacha::Field
|
|
64
71
|
@justification = :ljust
|
65
72
|
@output_conversion = [:to_s]
|
66
73
|
@fill_character = ' '
|
67
|
-
when /\$+\u00a2\u00a2/
|
68
|
-
@data_type = Nacha::Numeric
|
69
|
-
@justification = :rjust
|
70
|
-
@json_output = [[:to_i], [:/, 100.0]]
|
71
|
-
@output_conversion = [:to_i]
|
72
|
-
@fill_character = '0'
|
73
74
|
end
|
74
75
|
end
|
75
76
|
|
76
77
|
def data=(val)
|
77
78
|
@data = @data_type.new(val)
|
78
79
|
@input_data = val
|
80
|
+
rescue StandardError => e
|
81
|
+
add_error("Invalid data for #{@name}: \"#{val.inspect}\"")
|
82
|
+
add_error("Error: #{e.message}")
|
83
|
+
@data = String.new(val.to_s)
|
84
|
+
@input_data = val
|
79
85
|
end
|
80
86
|
|
81
87
|
def mandatory?
|
@@ -126,6 +132,25 @@ class Nacha::Field
|
|
126
132
|
end
|
127
133
|
end
|
128
134
|
|
135
|
+
def human_name
|
136
|
+
# @human_name ||= @name.to_s.gsub('_', ' ').capitalize
|
137
|
+
@human_name ||= @name.to_s.split('_').map(&:capitalize).join(' ')
|
138
|
+
end
|
139
|
+
|
140
|
+
def to_html
|
141
|
+
tooltip_text = "<span class=\"tooltiptext\" >#{human_name}</span>"
|
142
|
+
field_classes = ["nacha-field tooltip"]
|
143
|
+
field_classes += ['mandatory'] if mandatory?
|
144
|
+
field_classes += ['required'] if required?
|
145
|
+
field_classes += ['optional'] if optional?
|
146
|
+
field_classes += ['error'] if errors.any?
|
147
|
+
|
148
|
+
ach_string = to_ach.gsub(' ', ' ')
|
149
|
+
"<span data-field-name=\"#{@name}\" class=\"#{field_classes.join(' ')}\" data-name=\"#{@name}\">#{ach_string}" +
|
150
|
+
tooltip_text.to_s +
|
151
|
+
"</span>"
|
152
|
+
end
|
153
|
+
|
129
154
|
def to_s
|
130
155
|
@data.send(*output_conversion).to_s
|
131
156
|
end
|
data/lib/nacha/numeric.rb
CHANGED
@@ -6,8 +6,8 @@ class Nacha::Numeric
|
|
6
6
|
|
7
7
|
def to_i
|
8
8
|
if @value
|
9
|
-
if @value.is_a?(String) && @value.match(/
|
10
|
-
|
9
|
+
if @value.is_a?(String) && @value.match(/\A *\z/)
|
10
|
+
@value # blank strings should return as blank
|
11
11
|
else
|
12
12
|
@value.to_i
|
13
13
|
end
|
@@ -20,11 +20,14 @@ class Nacha::Numeric
|
|
20
20
|
if val.is_a?(String)
|
21
21
|
@value = val.dup
|
22
22
|
if(val.strip.length > 0)
|
23
|
-
@
|
24
|
-
@
|
23
|
+
@op_value = BigDecimal(val.strip)
|
24
|
+
@value = val.dup
|
25
25
|
else
|
26
26
|
@op_value = BigDecimal(0)
|
27
27
|
end
|
28
|
+
elsif val.nil?
|
29
|
+
@value = BigDecimal(0)
|
30
|
+
@op_value = @value
|
28
31
|
else
|
29
32
|
@value = BigDecimal(val)
|
30
33
|
@op_value = @value
|
@@ -32,7 +35,7 @@ class Nacha::Numeric
|
|
32
35
|
end
|
33
36
|
|
34
37
|
def to_s
|
35
|
-
@value ? @value.to_s : nil
|
38
|
+
@value ? @value.to_i.to_s : nil
|
36
39
|
end
|
37
40
|
|
38
41
|
def respond_to_missing?(method_name, include_private = false)
|
data/lib/nacha/parser.rb
CHANGED
@@ -1,35 +1,42 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'nacha'
|
4
|
+
require 'nacha/parser_context'
|
4
5
|
|
5
6
|
class Nacha::Parser
|
6
7
|
DEFAULT_RECORD_TYPES = ['Nacha::Record::FileHeader'].freeze
|
8
|
+
|
9
|
+
attr_reader :context
|
10
|
+
|
7
11
|
def initialize
|
12
|
+
@context = Nacha::ParserContext.new
|
8
13
|
reset!
|
9
14
|
end
|
10
15
|
|
11
16
|
def reset!; end
|
12
17
|
|
13
18
|
def parse_file(file)
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
records << process(line, line_num, records.last)
|
18
|
-
parent = records.last if records.lasts.class.child_record_types.any?
|
19
|
-
end
|
19
|
+
@context.parser_started_at = Time.now
|
20
|
+
@context.file_name = file
|
21
|
+
parse_string(file.read)
|
20
22
|
end
|
21
23
|
|
22
24
|
def parse_string(str)
|
23
25
|
line_num = -1
|
24
26
|
records = []
|
25
|
-
|
27
|
+
@context.parser_started_at ||= Time.now
|
28
|
+
str.scan(/(.{94}|(\A[^\n]+))/).each do |line|
|
29
|
+
line = line.first.strip
|
26
30
|
line_num += 1
|
31
|
+
@context.line_number = line_num
|
32
|
+
@context.line_length = line.length
|
27
33
|
records << process(line, line_num, records.last)
|
28
34
|
end.compact
|
29
35
|
records
|
30
36
|
end
|
31
37
|
|
32
38
|
def process(line, line_num, previous = nil)
|
39
|
+
@context.line_errors = []
|
33
40
|
parent = previous
|
34
41
|
record = nil
|
35
42
|
|
@@ -37,10 +44,11 @@ class Nacha::Parser
|
|
37
44
|
while record_types
|
38
45
|
record = parse_by_types(line, record_types)
|
39
46
|
break if record || !parent
|
40
|
-
|
47
|
+
record.validate if record
|
41
48
|
parent = parent.parent
|
42
49
|
record_types = valid_record_types(parent)
|
43
50
|
end
|
51
|
+
record.line_number = line_num if record
|
44
52
|
add_child(parent, record)
|
45
53
|
record
|
46
54
|
end
|
@@ -61,7 +69,8 @@ class Nacha::Parser
|
|
61
69
|
def parse_by_types(line, record_types)
|
62
70
|
record_types.detect do |rt|
|
63
71
|
record_type = Object.const_get(rt)
|
64
|
-
|
72
|
+
record = record_type.parse(line) if record_type.matcher =~ line
|
73
|
+
return record if record
|
65
74
|
end
|
66
75
|
end
|
67
76
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
class Nacha::ParserContext
|
5
|
+
attr_accessor :file_name
|
6
|
+
attr_accessor :line_number
|
7
|
+
attr_accessor :line_length
|
8
|
+
attr_accessor :line_errors
|
9
|
+
attr_accessor :parser_started_at
|
10
|
+
attr_accessor :parser_ended_at # nil 'till the end of parsing
|
11
|
+
attr_accessor :previous_record
|
12
|
+
attr_reader :validated
|
13
|
+
|
14
|
+
def initialize (opts = {})
|
15
|
+
@file_name = opts[:file_name] || ''
|
16
|
+
@line_number = opts[:line_number] || 0
|
17
|
+
@line_length = opts[:line_length] || 0
|
18
|
+
@current_line_errors = []
|
19
|
+
@parser_started_at = Time.now
|
20
|
+
@parser_ended_at = nil
|
21
|
+
@previous_record = nil
|
22
|
+
@validated = false
|
23
|
+
end
|
24
|
+
|
25
|
+
def validated?
|
26
|
+
@validated ||= false
|
27
|
+
end
|
28
|
+
|
29
|
+
def valid?
|
30
|
+
@valid ||= errors.empty?
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
data/lib/nacha/record/base.rb
CHANGED
@@ -10,10 +10,12 @@ module Nacha
|
|
10
10
|
include Validations::FieldValidations
|
11
11
|
|
12
12
|
attr_accessor :children, :parent, :fields
|
13
|
-
attr_reader :name, :validations
|
13
|
+
attr_reader :name, :validations, :errors
|
14
|
+
attr_accessor :line_number
|
14
15
|
|
15
16
|
def initialize(opts = {})
|
16
17
|
@children = []
|
18
|
+
@errors = []
|
17
19
|
create_fields_from_definition
|
18
20
|
opts.each do |k, v|
|
19
21
|
setter = "#{k}="
|
@@ -25,6 +27,96 @@ module Nacha
|
|
25
27
|
end
|
26
28
|
end
|
27
29
|
|
30
|
+
class << self
|
31
|
+
attr_reader :nacha_record_name
|
32
|
+
|
33
|
+
def nacha_field(name, inclusion:, contents:, position:)
|
34
|
+
definition[name] = { inclusion: inclusion,
|
35
|
+
contents: contents,
|
36
|
+
position: position,
|
37
|
+
name: name}
|
38
|
+
validation_method = "valid_#{name}".to_sym
|
39
|
+
return unless respond_to?(validation_method)
|
40
|
+
|
41
|
+
validations[name] ||= []
|
42
|
+
validations[name] << validation_method
|
43
|
+
end
|
44
|
+
def definition
|
45
|
+
@definition ||= {}
|
46
|
+
end
|
47
|
+
|
48
|
+
def validations
|
49
|
+
@validations ||= {}
|
50
|
+
end
|
51
|
+
|
52
|
+
def unpack_str
|
53
|
+
@unpack_str ||= definition.values.collect do |d|
|
54
|
+
Nacha::Field.unpack_str(d)
|
55
|
+
end.join.freeze
|
56
|
+
end
|
57
|
+
|
58
|
+
def old_matcher
|
59
|
+
@matcher ||=
|
60
|
+
Regexp.new('\A' + definition.values.collect do |d|
|
61
|
+
if d[:contents] =~ /\AC(.+)\z/ || d['contents'] =~ /\AC(.+)\z/
|
62
|
+
Regexp.last_match(1)
|
63
|
+
else
|
64
|
+
'.' * (d[:position] || d['position']).size
|
65
|
+
end
|
66
|
+
end.join + '\z')
|
67
|
+
end
|
68
|
+
|
69
|
+
# A more strict matcher that accounts for numeric and date fields
|
70
|
+
# and allows for spaces in those fields, but not alphabetic characters
|
71
|
+
# Also matches strings that might not be long enough.
|
72
|
+
#
|
73
|
+
# Processes the definition in reverse order to allow later fields to be
|
74
|
+
# skipped if they are not present in the input string.
|
75
|
+
def matcher
|
76
|
+
return @matcher if @matcher
|
77
|
+
|
78
|
+
output_started = false
|
79
|
+
skipped_output = false
|
80
|
+
@matcher ||=
|
81
|
+
Regexp.new('\A' + definition.values.reverse.collect do |d|
|
82
|
+
if d[:contents] =~ /\AC(.+)\z/
|
83
|
+
output_started = true
|
84
|
+
Regexp.last_match(1)
|
85
|
+
elsif d[:contents] =~ /\ANumeric\z/
|
86
|
+
if output_started
|
87
|
+
'[0-9 ]' + "{#{(d[:position] || d['position']).size}}"
|
88
|
+
else
|
89
|
+
skipped_output = true
|
90
|
+
''
|
91
|
+
end
|
92
|
+
elsif d[:contents] =~ /\AYYMMDD\z/
|
93
|
+
if output_started
|
94
|
+
'[0-9 ]' + "{#{(d[:position] || d['position']).size}}"
|
95
|
+
else
|
96
|
+
skipped_output = true
|
97
|
+
''
|
98
|
+
end
|
99
|
+
else
|
100
|
+
if output_started
|
101
|
+
'.' + "{#{(d[:position] || d['position']).size}}"
|
102
|
+
else
|
103
|
+
skipped_output = true
|
104
|
+
''
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end.reverse.join + (skipped_output ? '.*' : '') + '\z')
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
def parse(ach_str)
|
112
|
+
rec = new
|
113
|
+
ach_str.unpack(unpack_str).zip(rec.fields.values) do |input_data, field|
|
114
|
+
field.data = input_data
|
115
|
+
end
|
116
|
+
rec
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
28
120
|
def create_fields_from_definition
|
29
121
|
@fields ||= {}
|
30
122
|
definition.each_pair do |field_name, field_def|
|
@@ -36,6 +128,10 @@ module Nacha
|
|
36
128
|
Nacha.record_name(self.class)
|
37
129
|
end
|
38
130
|
|
131
|
+
def human_name
|
132
|
+
@human_name ||= record_type.to_s.split('_').map(&:capitalize).join(' ')
|
133
|
+
end
|
134
|
+
|
39
135
|
def to_h
|
40
136
|
{ nacha_record_type: record_type }.merge(
|
41
137
|
@fields.keys.map do |key|
|
@@ -53,32 +149,22 @@ module Nacha
|
|
53
149
|
end.join
|
54
150
|
end
|
55
151
|
|
56
|
-
def
|
57
|
-
|
58
|
-
end
|
59
|
-
|
60
|
-
def self.nacha_field(name, inclusion:, contents:, position:)
|
61
|
-
definition[name] = { inclusion: inclusion,
|
62
|
-
contents: contents,
|
63
|
-
position: position,
|
64
|
-
name: name}
|
65
|
-
validation_method = "valid_#{name}".to_sym
|
66
|
-
return unless respond_to?(validation_method)
|
152
|
+
def to_html(opts = {})
|
153
|
+
record_error_class = nil
|
67
154
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
@validations ||= {}
|
155
|
+
field_html = @fields.keys.collect do |key|
|
156
|
+
record_error_class ||= 'error' if @fields[key].errors.any?
|
157
|
+
@fields[key].to_html
|
158
|
+
end.join
|
159
|
+
"<div class=\"nacha-record tooltip #{record_type} #{record_error_class}\">" +
|
160
|
+
"<span class=\"nacha-field\" data-name=\"record-number\">#{"%05d" % [line_number]} | </span>" +
|
161
|
+
field_html +
|
162
|
+
"<span class=\"record-type\" data-name=\"record-type\">#{human_name}</span>" +
|
163
|
+
"</div>"
|
78
164
|
end
|
79
165
|
|
80
|
-
|
81
|
-
|
166
|
+
def inspect
|
167
|
+
"#<#{self.class.name}> #{to_h}"
|
82
168
|
end
|
83
169
|
|
84
170
|
def definition
|
@@ -86,64 +172,119 @@ module Nacha
|
|
86
172
|
end
|
87
173
|
|
88
174
|
def validate
|
89
|
-
self.class.definition.keys.map do |field|
|
90
|
-
next unless self.class.validations[field]
|
175
|
+
failing_checks = self.class.definition.keys.map do |field|
|
176
|
+
next true unless self.class.validations[field]
|
91
177
|
|
92
178
|
# rubocop:disable GitlabSecurity/PublicSend
|
93
179
|
field_data = send(field)
|
94
|
-
|
180
|
+
|
181
|
+
self.class.validations[field].map do |validation_method|
|
182
|
+
self.class.send(validation_method, field_data)
|
183
|
+
end
|
95
184
|
# rubocop:enable GitlabSecurity/PublicSend
|
96
|
-
end
|
185
|
+
end.flatten
|
97
186
|
end
|
98
187
|
|
99
188
|
# look for invalid fields, if none, then return true
|
100
189
|
def valid?
|
101
|
-
statuses = self.class.definition.keys.map do |field_sym|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
190
|
+
# statuses = self.class.definition.keys.map do |field_sym|
|
191
|
+
# # rubocop:disable GitlabSecurity/PublicSend
|
192
|
+
# field = send(field_sym)
|
193
|
+
# # rubocop:enable GitlabSecurity/PublicSend
|
194
|
+
# next true unless field.mandatory?
|
106
195
|
|
107
|
-
|
108
|
-
|
109
|
-
end
|
110
|
-
|
196
|
+
# ## TODO: levels of validity with 'R' and 'O' fields
|
197
|
+
# field.valid?
|
198
|
+
# end
|
199
|
+
statuses = validate
|
200
|
+
(statuses.detect { |valid| valid == false } != false)
|
111
201
|
end
|
112
202
|
|
203
|
+
# Checks if the current transaction code represents a debit transaction.
|
204
|
+
#
|
205
|
+
# This method evaluates the `transaction_code` (which is expected to be
|
206
|
+
# an attribute or method available in the current context) against a
|
207
|
+
# predefined set of debit transaction codes.
|
208
|
+
#
|
209
|
+
# @return [Boolean] `true` if `transaction_code` is present and its
|
210
|
+
# string representation is included in `DEBIT_TRANSACTION_CODES`,
|
211
|
+
# `false` otherwise.
|
212
|
+
#
|
213
|
+
# @example
|
214
|
+
# # Assuming transaction_code is "201" and DEBIT_TRANSACTION_CODES includes "201"
|
215
|
+
# debit? #=> true
|
216
|
+
#
|
217
|
+
# # Assuming transaction_code is "100" and DEBIT_TRANSACTION_CODES does not include "100"
|
218
|
+
# debit? #=> false
|
219
|
+
#
|
220
|
+
# # Assuming transaction_code is nil
|
221
|
+
# debit? #=> false
|
222
|
+
#
|
223
|
+
# @note
|
224
|
+
# This method includes robust error handling. If a `NoMethodError`
|
225
|
+
# occurs (e.g., if `transaction_code` is undefinable or does not respond
|
226
|
+
# to `to_s` in an unexpected way) or a `NameError` occurs (e.g., if
|
227
|
+
# `transaction_code` or `DEBIT_TRANSACTION_CODES` is not defined
|
228
|
+
# in the current scope), the method gracefully rescues these exceptions
|
229
|
+
# and returns `false`. This default behavior ensures that an inability
|
230
|
+
# to determine the transaction type results in it being considered
|
231
|
+
# "not a debit".
|
232
|
+
#
|
233
|
+
# @see #transaction_code (if `transaction_code` is an instance method or attribute)
|
234
|
+
# @see DEBIT_TRANSACTION_CODES (the constant defining debit codes)
|
113
235
|
def debit?
|
114
236
|
transaction_code &&
|
115
237
|
DEBIT_TRANSACTION_CODES.include?(transaction_code.to_s)
|
238
|
+
rescue NoMethodError, NameError
|
239
|
+
false
|
116
240
|
end
|
117
241
|
|
242
|
+
# Checks if the current transaction code represents a credit transaction.
|
243
|
+
#
|
244
|
+
# This method evaluates the `transaction_code` (which is expected to be
|
245
|
+
# an attribute or method available in the current context) against a
|
246
|
+
# predefined set of credit transaction codes.
|
247
|
+
#
|
248
|
+
# @return [Boolean] `true` if `transaction_code` is present and its
|
249
|
+
# string representation is included in `CREDIT_TRANSACTION_CODES`,
|
250
|
+
# `false` otherwise.
|
251
|
+
#
|
252
|
+
# @example
|
253
|
+
# # Assuming transaction_code is "101" and CREDIT_TRANSACTION_CODES includes "101"
|
254
|
+
# credit? #=> true
|
255
|
+
#
|
256
|
+
# # Assuming transaction_code is "200" and CREDIT_TRANSACTION_CODES does not include "200"
|
257
|
+
# credit? #=> false
|
258
|
+
#
|
259
|
+
# # Assuming transaction_code is nil
|
260
|
+
# credit? #=> false
|
261
|
+
#
|
262
|
+
# @note
|
263
|
+
# This method includes robust error handling. If a `NoMethodError`
|
264
|
+
# occurs (e.g., if `transaction_code` is undefinable or does not respond
|
265
|
+
# to `to_s` in an unexpected way) or a `NameError` occurs (e.g., if
|
266
|
+
# `transaction_code` or `CREDIT_TRANSACTION_CODES` is not defined
|
267
|
+
# in the current scope), the method gracefully rescues these exceptions
|
268
|
+
# and returns `false`. This default behavior ensures that an inability
|
269
|
+
# to determine the transaction type results in it being considered
|
270
|
+
# "not a credit".
|
271
|
+
#
|
272
|
+
# @see #transaction_code (if `transaction_code` is an instance method or attribute)
|
273
|
+
# @see CREDIT_TRANSACTION_CODES (the constant defining credit codes)
|
118
274
|
def credit?
|
119
275
|
transaction_code &&
|
120
276
|
CREDIT_TRANSACTION_CODES.include?(transaction_code.to_s)
|
277
|
+
rescue NoMethodError, NameError
|
278
|
+
false
|
121
279
|
end
|
122
280
|
|
123
|
-
def
|
124
|
-
@
|
125
|
-
Nacha::Field.unpack_str(d)
|
126
|
-
end.join.freeze
|
281
|
+
def errors
|
282
|
+
(@errors + @fields.values.map { |field| field.errors }).flatten
|
127
283
|
end
|
128
284
|
|
129
|
-
def self.matcher
|
130
|
-
# puts definition.keys.join(', ')
|
131
|
-
definition['matcher'] ||
|
132
|
-
Regexp.new('\A' + definition.values.collect do |d|
|
133
|
-
if d[:contents] =~ /\AC(.+)\z/ || d['contents'] =~ /\AC(.+)\z/
|
134
|
-
Regexp.last_match(1)
|
135
|
-
else
|
136
|
-
'.' * (d[:position] || d['position']).size
|
137
|
-
end
|
138
|
-
end.join + '\z')
|
139
|
-
end
|
140
285
|
|
141
|
-
def
|
142
|
-
|
143
|
-
ach_str.unpack(unpack_str).zip(rec.fields.values) do |input_data, field|
|
144
|
-
field.data = input_data
|
145
|
-
end
|
146
|
-
rec
|
286
|
+
def add_error(err_string)
|
287
|
+
@errors << err_string
|
147
288
|
end
|
148
289
|
|
149
290
|
def respond_to?(method_name, include_private = false)
|
data/lib/nacha/record/filler.rb
CHANGED
@@ -8,6 +8,9 @@ module Nacha
|
|
8
8
|
module Record
|
9
9
|
class Filler < Nacha::Record::Base
|
10
10
|
include FillerRecordType
|
11
|
+
|
12
|
+
nacha_field :record_type_code, inclusion: 'M', contents: ('C' + ('9' * 93)), position: 1..1
|
13
|
+
nacha_field :filler, inclusion: 'M', contents: 'Numeric', position: 2..94
|
11
14
|
end
|
12
15
|
end
|
13
16
|
end
|
@@ -10,24 +10,34 @@ module Nacha
|
|
10
10
|
end
|
11
11
|
module ClassMethods
|
12
12
|
def check_field_error(field, message = nil, condition = nil)
|
13
|
-
(block_given? ? yield : condition) || (field.add_error("'#{field.
|
13
|
+
(block_given? ? yield : condition) || (field.add_error("'#{field.name}' '#{field}' is invalid") && false)
|
14
14
|
end
|
15
15
|
|
16
16
|
def valid_service_class_code field
|
17
|
-
check_field_error(field
|
17
|
+
check_field_error(field, "'#{field.name}' '#{field}' should be one of #{SERVICE_CLASS_CODES.join(', ')}") {
|
18
|
+
SERVICE_CLASS_CODES.include? field.to_s
|
19
|
+
}
|
18
20
|
end
|
19
21
|
|
20
22
|
def valid_standard_entry_class_code field
|
21
|
-
check_field_error(field
|
23
|
+
check_field_error(field, "'#{field.name}' '#{field}' should be one of #{STANDARD_ENTRY_CLASS_CODES.join(', ')}") {
|
24
|
+
STANDARD_ENTRY_CLASS_CODES.include? field.data
|
25
|
+
}
|
22
26
|
end
|
23
27
|
|
24
28
|
def valid_transaction_code field
|
25
|
-
check_field_error(field
|
29
|
+
check_field_error(field, "'#{field.name}' '#{field}' should be one of #{TRANSACTION_CODES.join(', ')}") {
|
30
|
+
TRANSACTION_CODES.include? field.to_s
|
31
|
+
}
|
26
32
|
end
|
27
33
|
|
28
34
|
def valid_receiving_dfi_identification field
|
29
35
|
check_field_error(field) { field.valid? }
|
30
36
|
end
|
37
|
+
|
38
|
+
def valid_filler field
|
39
|
+
check_field_error(field) { field.to_s == ('9' * 93) }
|
40
|
+
end
|
31
41
|
end
|
32
42
|
end
|
33
43
|
end
|
data/lib/nacha/version.rb
CHANGED
data/lib/nacha.rb
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'nacha/version'
|
4
3
|
require 'yaml'
|
5
4
|
require 'nacha/aba_number'
|
6
5
|
require 'nacha/ach_date'
|
7
6
|
require 'nacha/field'
|
8
7
|
require 'nacha/numeric'
|
8
|
+
require 'nacha/version'
|
9
9
|
|
10
10
|
Gem.find_files('nacha/record/*.rb').reject{|f| f =~ /\/spec\//}.each do |file|
|
11
11
|
require File.expand_path(file)
|
data/nacha.gemspec
CHANGED
@@ -24,6 +24,8 @@ Gem::Specification.new do |spec|
|
|
24
24
|
spec.add_dependency 'bigdecimal'
|
25
25
|
|
26
26
|
spec.add_development_dependency "bundler", "~> 2.0"
|
27
|
+
spec.add_development_dependency "byebug"
|
28
|
+
spec.add_development_dependency "pry"
|
27
29
|
spec.add_development_dependency "factory_bot"
|
28
30
|
spec.add_development_dependency "gitlab-styles"
|
29
31
|
spec.add_development_dependency "guard"
|
@@ -33,4 +35,5 @@ Gem::Specification.new do |spec|
|
|
33
35
|
spec.add_development_dependency "rubocop"
|
34
36
|
spec.add_development_dependency 'rubocop-performance'
|
35
37
|
spec.add_development_dependency 'rubocop-rspec'
|
38
|
+
spec.add_development_dependency 'simplecov'
|
36
39
|
end
|
@@ -0,0 +1 @@
|
|
1
|
+
</body></html>
|
@@ -0,0 +1,88 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang='en'>
|
3
|
+
<head>
|
4
|
+
<meta charset='UTF-8'>
|
5
|
+
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
|
6
|
+
<title>NACHA File Parser</title>
|
7
|
+
|
8
|
+
<style>
|
9
|
+
body {
|
10
|
+
font-family: Arial, sans-serif;
|
11
|
+
}
|
12
|
+
body, h1, h2, h3, p {
|
13
|
+
margin: 0;
|
14
|
+
padding: 0;
|
15
|
+
}
|
16
|
+
body {
|
17
|
+
padding: 20px;
|
18
|
+
}
|
19
|
+
h1 {
|
20
|
+
|
21
|
+
font-size: 24px;
|
22
|
+
margin-bottom: 20px;
|
23
|
+
}
|
24
|
+
|
25
|
+
span.nacha-field:hover {
|
26
|
+
background-color: yellow;
|
27
|
+
}
|
28
|
+
|
29
|
+
.tooltip {
|
30
|
+
position: relative;
|
31
|
+
display: inline-block;
|
32
|
+
/* border-bottom: 1px dotted black; *//* If you want dots under the hoverable text */
|
33
|
+
}
|
34
|
+
|
35
|
+
/* Tooltip text */
|
36
|
+
.tooltip > .tooltiptext {
|
37
|
+
visibility: hidden;
|
38
|
+
/* width: 120px; */
|
39
|
+
background-color: black;
|
40
|
+
color: #fff;
|
41
|
+
text-align: center;
|
42
|
+
padding: 5px 5px;
|
43
|
+
border-radius: 6px;
|
44
|
+
|
45
|
+
top: 100%;
|
46
|
+
left: 50%;
|
47
|
+
|
48
|
+
/* Position the tooltip text - see examples below! */
|
49
|
+
position: absolute;
|
50
|
+
z-index: 1;
|
51
|
+
}
|
52
|
+
|
53
|
+
.nacha-record {
|
54
|
+
font-family: monospace;
|
55
|
+
}
|
56
|
+
|
57
|
+
.nacha-field {
|
58
|
+
display: inline-block;
|
59
|
+
background-color: #f0f0f0;
|
60
|
+
}
|
61
|
+
.nacha-field.error {
|
62
|
+
background-color: pink;
|
63
|
+
}
|
64
|
+
}
|
65
|
+
|
66
|
+
.nacha-record > .tooltiptext {
|
67
|
+
top: -50%;
|
68
|
+
left: 100%;
|
69
|
+
background-color: gray;
|
70
|
+
}
|
71
|
+
|
72
|
+
.record-type {
|
73
|
+
padding-left: 5px;
|
74
|
+
font-weight: bold;
|
75
|
+
color: #333;
|
76
|
+
}
|
77
|
+
|
78
|
+
|
79
|
+
/* Show the tooltip text when you mouse over the tooltip container */
|
80
|
+
.tooltip:hover > .tooltiptext {
|
81
|
+
visibility: visible;
|
82
|
+
}
|
83
|
+
|
84
|
+
|
85
|
+
</style>
|
86
|
+
</head>
|
87
|
+
<body>
|
88
|
+
<h1>NACHA File Parser</h1>
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: nacha
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- David H. Wilkins
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: bigdecimal
|
@@ -37,6 +37,34 @@ dependencies:
|
|
37
37
|
- - "~>"
|
38
38
|
- !ruby/object:Gem::Version
|
39
39
|
version: '2.0'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: byebug
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
type: :development
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: pry
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
type: :development
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
40
68
|
- !ruby/object:Gem::Dependency
|
41
69
|
name: factory_bot
|
42
70
|
requirement: !ruby/object:Gem::Requirement
|
@@ -163,10 +191,25 @@ dependencies:
|
|
163
191
|
- - ">="
|
164
192
|
- !ruby/object:Gem::Version
|
165
193
|
version: '0'
|
194
|
+
- !ruby/object:Gem::Dependency
|
195
|
+
name: simplecov
|
196
|
+
requirement: !ruby/object:Gem::Requirement
|
197
|
+
requirements:
|
198
|
+
- - ">="
|
199
|
+
- !ruby/object:Gem::Version
|
200
|
+
version: '0'
|
201
|
+
type: :development
|
202
|
+
prerelease: false
|
203
|
+
version_requirements: !ruby/object:Gem::Requirement
|
204
|
+
requirements:
|
205
|
+
- - ">="
|
206
|
+
- !ruby/object:Gem::Version
|
207
|
+
version: '0'
|
166
208
|
description: Ruby parser for ACH files.
|
167
209
|
email:
|
168
210
|
- dwilkins@conecuh.com
|
169
|
-
executables:
|
211
|
+
executables:
|
212
|
+
- nacha
|
170
213
|
extensions: []
|
171
214
|
extra_rdoc_files: []
|
172
215
|
files:
|
@@ -185,12 +228,15 @@ files:
|
|
185
228
|
- Rakefile
|
186
229
|
- bin/console
|
187
230
|
- bin/setup
|
231
|
+
- exe/nacha
|
188
232
|
- lib/nacha.rb
|
189
233
|
- lib/nacha/aba_number.rb
|
190
234
|
- lib/nacha/ach_date.rb
|
191
235
|
- lib/nacha/field.rb
|
236
|
+
- lib/nacha/has_errors.rb
|
192
237
|
- lib/nacha/numeric.rb
|
193
238
|
- lib/nacha/parser.rb
|
239
|
+
- lib/nacha/parser_context.rb
|
194
240
|
- lib/nacha/record/addenda_record_type.rb
|
195
241
|
- lib/nacha/record/adv_batch_control.rb
|
196
242
|
- lib/nacha/record/adv_entry_detail.rb
|
@@ -252,6 +298,8 @@ files:
|
|
252
298
|
- lib/nacha/record/xck_entry_detail.rb
|
253
299
|
- lib/nacha/version.rb
|
254
300
|
- nacha.gemspec
|
301
|
+
- templates/html_postamble.html
|
302
|
+
- templates/html_preamble.html
|
255
303
|
homepage: https://github.com/dwilkins/nacha
|
256
304
|
licenses:
|
257
305
|
- MIT
|
@@ -270,7 +318,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
270
318
|
- !ruby/object:Gem::Version
|
271
319
|
version: '0'
|
272
320
|
requirements: []
|
273
|
-
rubygems_version: 3.6.
|
321
|
+
rubygems_version: 3.6.7
|
274
322
|
specification_version: 4
|
275
323
|
summary: Ruby parser for ACH files.
|
276
324
|
test_files: []
|