nacha 0.1.2 → 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 +18 -0
- data/exe/nacha +20 -21
- data/lib/nacha/aba_number.rb +23 -1
- data/lib/nacha/field.rb +19 -8
- data/lib/nacha/has_errors.rb +12 -0
- data/lib/nacha/numeric.rb +8 -5
- data/lib/nacha/parser.rb +16 -3
- data/lib/nacha/parser_context.rb +33 -0
- data/lib/nacha/record/base.rb +196 -65
- 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_preamble.html +66 -46
- metadata +45 -1
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
@@ -61,6 +61,24 @@ API may change at any time. Pull requests welcomed
|
|
61
61
|
```
|
62
62
|
nacha parse ach_file.ach > ach_file.html`
|
63
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.
|
81
|
+
|
64
82
|
## Development
|
65
83
|
|
66
84
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/exe/nacha
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
2
3
|
|
3
4
|
require 'thor'
|
4
5
|
require 'nacha' # Assuming this loads the Nacha gem
|
5
6
|
|
6
7
|
module Nacha
|
7
8
|
class CLI < Thor
|
8
|
-
|
9
9
|
TEMPLATES_DIR = File.join(Gem::Specification.find_by_name("nacha").gem_dir,
|
10
10
|
"templates").freeze
|
11
11
|
|
@@ -14,26 +14,24 @@ module Nacha
|
|
14
14
|
|
15
15
|
desc "parse FILE", "Parse an ACH file"
|
16
16
|
def parse(file_path)
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
end
|
17
|
+
unless File.exist?(file_path)
|
18
|
+
puts "Error: File not found at #{file_path}"
|
19
|
+
exit 1
|
20
|
+
end
|
22
21
|
|
23
|
-
|
22
|
+
ach_file = [Nacha.parse(File.open(file_path)).first] # Use Nacha.parse
|
24
23
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
end
|
32
|
-
rescue StandardError => e
|
33
|
-
puts "An error occurred during parsing: #{e.message}"
|
34
|
-
puts e.backtrace.join("\n")
|
35
|
-
exit 1
|
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."
|
36
30
|
end
|
31
|
+
rescue StandardError => e
|
32
|
+
puts "An error occurred during parsing: #{e.message}"
|
33
|
+
puts e.backtrace.join("\n")
|
34
|
+
exit 1
|
37
35
|
end
|
38
36
|
|
39
37
|
private
|
@@ -41,7 +39,7 @@ module Nacha
|
|
41
39
|
def output_html(file_path, ach_file)
|
42
40
|
puts html_preamble
|
43
41
|
puts "<h1>Successfully parsed #{file_path}</h1>\n"
|
44
|
-
display_child(0, ach_file.first
|
42
|
+
display_child(0, ach_file.first) # Display the first record
|
45
43
|
puts html_postamble
|
46
44
|
end
|
47
45
|
|
@@ -53,16 +51,17 @@ module Nacha
|
|
53
51
|
@html_postamble ||= File.read(HTML_POSTAMBLE_FILE)
|
54
52
|
end
|
55
53
|
|
56
|
-
def display_child(level, record
|
54
|
+
def display_child(level, record)
|
57
55
|
# Attempt to call a summary or to_s method if it exists,
|
58
56
|
# otherwise inspect the record.
|
57
|
+
return unless record
|
59
58
|
level_indent = ' ' * level.to_i
|
60
59
|
puts "<html>"
|
61
60
|
puts record.to_html
|
62
61
|
if record.respond_to?(:children) && record.children.any?
|
63
62
|
if record.children.any?
|
64
63
|
record.children.each_with_index do |child_record, child_index|
|
65
|
-
display_child(level + 1, child_record
|
64
|
+
display_child(level + 1, child_record)
|
66
65
|
end
|
67
66
|
end
|
68
67
|
end
|
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?
|
@@ -133,9 +139,14 @@ class Nacha::Field
|
|
133
139
|
|
134
140
|
def to_html
|
135
141
|
tooltip_text = "<span class=\"tooltiptext\" >#{human_name}</span>"
|
136
|
-
field_classes = "nacha-field tooltip
|
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
|
+
|
137
148
|
ach_string = to_ach.gsub(' ', ' ')
|
138
|
-
"<span class=\"#{field_classes}\" data-name=\"#{@name}\">#{ach_string}" +
|
149
|
+
"<span data-field-name=\"#{@name}\" class=\"#{field_classes.join(' ')}\" data-name=\"#{@name}\">#{ach_string}" +
|
139
150
|
tooltip_text.to_s +
|
140
151
|
"</span>"
|
141
152
|
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,30 +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)
|
19
|
+
@context.parser_started_at = Time.now
|
20
|
+
@context.file_name = file
|
14
21
|
parse_string(file.read)
|
15
22
|
end
|
16
23
|
|
17
24
|
def parse_string(str)
|
18
25
|
line_num = -1
|
19
26
|
records = []
|
20
|
-
|
27
|
+
@context.parser_started_at ||= Time.now
|
28
|
+
str.scan(/(.{94}|(\A[^\n]+))/).each do |line|
|
29
|
+
line = line.first.strip
|
21
30
|
line_num += 1
|
31
|
+
@context.line_number = line_num
|
32
|
+
@context.line_length = line.length
|
22
33
|
records << process(line, line_num, records.last)
|
23
34
|
end.compact
|
24
35
|
records
|
25
36
|
end
|
26
37
|
|
27
38
|
def process(line, line_num, previous = nil)
|
39
|
+
@context.line_errors = []
|
28
40
|
parent = previous
|
29
41
|
record = nil
|
30
42
|
|
@@ -32,7 +44,7 @@ class Nacha::Parser
|
|
32
44
|
while record_types
|
33
45
|
record = parse_by_types(line, record_types)
|
34
46
|
break if record || !parent
|
35
|
-
|
47
|
+
record.validate if record
|
36
48
|
parent = parent.parent
|
37
49
|
record_types = valid_record_types(parent)
|
38
50
|
end
|
@@ -57,7 +69,8 @@ class Nacha::Parser
|
|
57
69
|
def parse_by_types(line, record_types)
|
58
70
|
record_types.detect do |rt|
|
59
71
|
record_type = Object.const_get(rt)
|
60
|
-
|
72
|
+
record = record_type.parse(line) if record_type.matcher =~ line
|
73
|
+
return record if record
|
61
74
|
end
|
62
75
|
end
|
63
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,11 +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
14
|
attr_accessor :line_number
|
15
15
|
|
16
16
|
def initialize(opts = {})
|
17
17
|
@children = []
|
18
|
+
@errors = []
|
18
19
|
create_fields_from_definition
|
19
20
|
opts.each do |k, v|
|
20
21
|
setter = "#{k}="
|
@@ -26,6 +27,96 @@ module Nacha
|
|
26
27
|
end
|
27
28
|
end
|
28
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
|
+
|
29
120
|
def create_fields_from_definition
|
30
121
|
@fields ||= {}
|
31
122
|
definition.each_pair do |field_name, field_def|
|
@@ -37,6 +128,10 @@ module Nacha
|
|
37
128
|
Nacha.record_name(self.class)
|
38
129
|
end
|
39
130
|
|
131
|
+
def human_name
|
132
|
+
@human_name ||= record_type.to_s.split('_').map(&:capitalize).join(' ')
|
133
|
+
end
|
134
|
+
|
40
135
|
def to_h
|
41
136
|
{ nacha_record_type: record_type }.merge(
|
42
137
|
@fields.keys.map do |key|
|
@@ -54,106 +149,142 @@ module Nacha
|
|
54
149
|
end.join
|
55
150
|
end
|
56
151
|
|
57
|
-
def to_html
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
152
|
+
def to_html(opts = {})
|
153
|
+
record_error_class = nil
|
154
|
+
|
155
|
+
field_html = @fields.keys.collect do |key|
|
156
|
+
record_error_class ||= 'error' if @fields[key].errors.any?
|
62
157
|
@fields[key].to_html
|
63
|
-
end.join
|
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>"
|
64
164
|
end
|
65
165
|
|
66
166
|
def inspect
|
67
167
|
"#<#{self.class.name}> #{to_h}"
|
68
168
|
end
|
69
169
|
|
70
|
-
def self.nacha_field(name, inclusion:, contents:, position:)
|
71
|
-
definition[name] = { inclusion: inclusion,
|
72
|
-
contents: contents,
|
73
|
-
position: position,
|
74
|
-
name: name}
|
75
|
-
validation_method = "valid_#{name}".to_sym
|
76
|
-
return unless respond_to?(validation_method)
|
77
|
-
|
78
|
-
validations[name] ||= []
|
79
|
-
validations[name] << validation_method
|
80
|
-
end
|
81
|
-
|
82
|
-
def self.definition
|
83
|
-
@definition ||= {}
|
84
|
-
end
|
85
|
-
|
86
|
-
def self.validations
|
87
|
-
@validations ||= {}
|
88
|
-
end
|
89
|
-
|
90
|
-
class << self
|
91
|
-
attr_reader :nacha_record_name
|
92
|
-
end
|
93
|
-
|
94
170
|
def definition
|
95
171
|
self.class.definition
|
96
172
|
end
|
97
173
|
|
98
174
|
def validate
|
99
|
-
self.class.definition.keys.map do |field|
|
100
|
-
next unless self.class.validations[field]
|
175
|
+
failing_checks = self.class.definition.keys.map do |field|
|
176
|
+
next true unless self.class.validations[field]
|
101
177
|
|
102
178
|
# rubocop:disable GitlabSecurity/PublicSend
|
103
179
|
field_data = send(field)
|
104
|
-
|
180
|
+
|
181
|
+
self.class.validations[field].map do |validation_method|
|
182
|
+
self.class.send(validation_method, field_data)
|
183
|
+
end
|
105
184
|
# rubocop:enable GitlabSecurity/PublicSend
|
106
|
-
end
|
185
|
+
end.flatten
|
107
186
|
end
|
108
187
|
|
109
188
|
# look for invalid fields, if none, then return true
|
110
189
|
def valid?
|
111
|
-
statuses = self.class.definition.keys.map do |field_sym|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
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?
|
116
195
|
|
117
|
-
|
118
|
-
|
119
|
-
end
|
120
|
-
|
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)
|
121
201
|
end
|
122
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)
|
123
235
|
def debit?
|
124
236
|
transaction_code &&
|
125
237
|
DEBIT_TRANSACTION_CODES.include?(transaction_code.to_s)
|
238
|
+
rescue NoMethodError, NameError
|
239
|
+
false
|
126
240
|
end
|
127
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)
|
128
274
|
def credit?
|
129
275
|
transaction_code &&
|
130
276
|
CREDIT_TRANSACTION_CODES.include?(transaction_code.to_s)
|
277
|
+
rescue NoMethodError, NameError
|
278
|
+
false
|
131
279
|
end
|
132
280
|
|
133
|
-
def
|
134
|
-
@
|
135
|
-
Nacha::Field.unpack_str(d)
|
136
|
-
end.join.freeze
|
281
|
+
def errors
|
282
|
+
(@errors + @fields.values.map { |field| field.errors }).flatten
|
137
283
|
end
|
138
284
|
|
139
|
-
def self.matcher
|
140
|
-
# puts definition.keys.join(', ')
|
141
|
-
definition['matcher'] ||
|
142
|
-
Regexp.new('\A' + definition.values.collect do |d|
|
143
|
-
if d[:contents] =~ /\AC(.+)\z/ || d['contents'] =~ /\AC(.+)\z/
|
144
|
-
Regexp.last_match(1)
|
145
|
-
else
|
146
|
-
'.' * (d[:position] || d['position']).size
|
147
|
-
end
|
148
|
-
end.join + '\z')
|
149
|
-
end
|
150
285
|
|
151
|
-
def
|
152
|
-
|
153
|
-
ach_str.unpack(unpack_str).zip(rec.fields.values) do |input_data, field|
|
154
|
-
field.data = input_data
|
155
|
-
end
|
156
|
-
rec
|
286
|
+
def add_error(err_string)
|
287
|
+
@errors << err_string
|
157
288
|
end
|
158
289
|
|
159
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
|
@@ -6,60 +6,80 @@
|
|
6
6
|
<title>NACHA File Parser</title>
|
7
7
|
|
8
8
|
<style>
|
9
|
-
body {
|
10
|
-
|
11
|
-
}
|
12
|
-
body, h1, h2, h3, p {
|
13
|
-
|
14
|
-
|
15
|
-
}
|
16
|
-
body {
|
17
|
-
|
18
|
-
}
|
19
|
-
h1 {
|
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
20
|
|
21
|
-
|
22
|
-
|
23
|
-
}
|
21
|
+
font-size: 24px;
|
22
|
+
margin-bottom: 20px;
|
23
|
+
}
|
24
24
|
|
25
|
-
span.nacha-field:hover {
|
26
|
-
|
27
|
-
}
|
25
|
+
span.nacha-field:hover {
|
26
|
+
background-color: yellow;
|
27
|
+
}
|
28
28
|
|
29
|
-
.tooltip {
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
}
|
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
34
|
|
35
|
-
/* Tooltip text */
|
36
|
-
.tooltip > .tooltiptext {
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
44
|
|
45
|
-
|
46
|
-
|
45
|
+
top: 100%;
|
46
|
+
left: 50%;
|
47
47
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
}
|
48
|
+
/* Position the tooltip text - see examples below! */
|
49
|
+
position: absolute;
|
50
|
+
z-index: 1;
|
51
|
+
}
|
52
52
|
|
53
|
-
.nacha-record
|
54
|
-
|
55
|
-
|
56
|
-
background-color: gray;
|
57
|
-
}
|
53
|
+
.nacha-record {
|
54
|
+
font-family: monospace;
|
55
|
+
}
|
58
56
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
}
|
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
|
+
}
|
63
83
|
|
64
84
|
|
65
85
|
</style>
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
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
|
@@ -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,6 +191,20 @@ 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
|
@@ -191,8 +233,10 @@ files:
|
|
191
233
|
- lib/nacha/aba_number.rb
|
192
234
|
- lib/nacha/ach_date.rb
|
193
235
|
- lib/nacha/field.rb
|
236
|
+
- lib/nacha/has_errors.rb
|
194
237
|
- lib/nacha/numeric.rb
|
195
238
|
- lib/nacha/parser.rb
|
239
|
+
- lib/nacha/parser_context.rb
|
196
240
|
- lib/nacha/record/addenda_record_type.rb
|
197
241
|
- lib/nacha/record/adv_batch_control.rb
|
198
242
|
- lib/nacha/record/adv_entry_detail.rb
|