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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d6d958fcc8edde96b13f6545611fb4fc86a751f4945955fe2f4960344bacc1ff
4
- data.tar.gz: 6b9cbe9d4d61638f847595d76316b78263bf7b807ef1ed6c78f11e66348c7bc6
3
+ metadata.gz: 0f9c3cffdb4484e108a60fb3f34a59ddff273b1d519fc7e5da6dabbe3a56c49a
4
+ data.tar.gz: 8e2ec6f4a3a18b795e4cd9906942262766439bab0b6f309a95f57f7fe7931676
5
5
  SHA512:
6
- metadata.gz: 4d559ecb5008d9e71e5c958dbc3400d35da7247c2a3f85f727adf50be020ac56c6b0ca30d470eff1247f45df05e94a603bc0754192ed7fc0baeee937abcd32ee
7
- data.tar.gz: 718bebf3b746d052cfa2ad0c4e3390b1fef291dd16a588f25e493b1ef9f336c97ad4392951e6fe743aa5e4085430bed2b649940f7c2770733abc31c4f70b3c1e
6
+ metadata.gz: ac4490619d1bf8be9ea04b8ee4544e0498a2c59921a4a6c374dfa3d659ea17f4b03d03947f84ad630859511358da9e1f3a4e5992d53106a9285f903b8b5ea791
7
+ data.tar.gz: ef3dc1c6be90957f1fc056fc532235e3b8841ab10a259e507b1aebacd784997db57f65630a703bbbd06d57b067a670697045eca088e0c268775ad43953a4f8cb
data/.gitignore CHANGED
@@ -7,7 +7,9 @@
7
7
  /pkg/
8
8
  /spec/reports/
9
9
  /tmp/
10
+ /vendor/bundle/
10
11
 
11
12
  # rspec failure tracking
12
13
  .rspec_status
13
- .byebug_history
14
+ .byebug_history
15
+ .aider*
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}) { |m| "spec/nacha/record_types/#{m[1]}_spec.rb" }
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
- begin
18
- unless File.exist?(file_path)
19
- puts "Error: File not found at #{file_path}"
20
- exit 1
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
- ach_file = [Nacha.parse(File.open(file_path)).first] # Use Nacha.parse
22
+ ach_file = [Nacha.parse(File.open(file_path)).first] # Use Nacha.parse
24
23
 
25
- # TODO: Determine a user-friendly way to output the parsed data.
26
- # For now, let's print the records.
27
- if ach_file && ach_file.is_a?(Array) && !ach_file.empty?
28
- output_html(file_path, ach_file)
29
- else
30
- puts "Could not parse the file or the file was empty."
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, 0) # Display the first record
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, index)
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, child_index)
64
+ display_child(level + 1, child_record)
66
65
  end
67
66
  end
68
67
  end
@@ -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 ||= (@routing_number.length == 9 && compute_check_digit == @routing_number.chars[8])
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 data-field-name=\"#{@name}\""
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(' ', '&nbsp;')
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
@@ -0,0 +1,12 @@
1
+ module HasErrors
2
+ attr_reader :errors
3
+
4
+ def add_error(message)
5
+ @errors ||= []
6
+ @errors << message
7
+ end
8
+
9
+ def has_errors?
10
+ !@errors.nil? && !@errors.empty?
11
+ end
12
+ 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
- self
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
- @value = BigDecimal(val.strip)
24
- @op_value = @value
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
- str.scan(/.{94}/).each do |line|
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
- return record_type.parse(line) if record_type.matcher =~ line
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
@@ -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
- "<div style=\"font-family: monospace;\"class='nacha-record tooltip #{record_type}'>" +
59
- "<span class='tooltiptext'>#{record_type}</span>" +
60
- "<span class='nacha-field' data-name='record-number'>#{"%05d" % [line_number]}&nbsp;|&nbsp</span>" +
61
- @fields.keys.collect do |key|
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 + "</div>"
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]}&nbsp;|&nbsp</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
- send(self.class.validations[:field], field_data)
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
- # rubocop:disable GitlabSecurity/PublicSend
113
- field = send(field_sym)
114
- # rubocop:enable GitlabSecurity/PublicSend
115
- next true unless field.mandatory?
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
- ## TODO: levels of validity with 'R' and 'O' fields
118
- field.valid?
119
- end
120
- !statuses.detect { |valid| valid == false }
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 self.unpack_str
134
- @unpack_str ||= definition.values.collect do |d|
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 self.parse(ach_str)
152
- rec = new
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)
@@ -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
@@ -8,7 +8,7 @@ module Nacha
8
8
  end
9
9
 
10
10
  module ClassMethods
11
- def self.child_record_types
11
+ def child_record_types
12
12
  []
13
13
  end
14
14
  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.data}' is invalid") && false)
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) { SERVICE_CLASS_CODES.include? field.to_s }
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) { STANDARD_ENTRY_CLASS_CODES.include? field.data }
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) { TRANSACTION_CODES.include? field.to_s }
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nacha
4
- VERSION = '0.1.2'
4
+ VERSION = '0.1.3'
5
5
  end
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
- 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 {
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
- font-size: 24px;
22
- margin-bottom: 20px;
23
- }
21
+ font-size: 24px;
22
+ margin-bottom: 20px;
23
+ }
24
24
 
25
- span.nacha-field:hover {
26
- background-color: yellow;
27
- }
25
+ span.nacha-field:hover {
26
+ background-color: yellow;
27
+ }
28
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
- }
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
- visibility: hidden;
38
- /* width: 120px; */
39
- background-color: black;
40
- color: #fff;
41
- text-align: center;
42
- padding: 5px 5px;
43
- border-radius: 6px;
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
- top: 100%;
46
- left: 50%;
45
+ top: 100%;
46
+ left: 50%;
47
47
 
48
- /* Position the tooltip text - see examples below! */
49
- position: absolute;
50
- z-index: 1;
51
- }
48
+ /* Position the tooltip text - see examples below! */
49
+ position: absolute;
50
+ z-index: 1;
51
+ }
52
52
 
53
- .nacha-record > .tooltiptext {
54
- top: -50%;
55
- left: 100%;
56
- background-color: gray;
57
- }
53
+ .nacha-record {
54
+ font-family: monospace;
55
+ }
58
56
 
59
- /* Show the tooltip text when you mouse over the tooltip container */
60
- .tooltip:hover > .tooltiptext {
61
- visibility: visible;
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.2
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