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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4ab30c59682e44d667d8341db3d7caa948d310231f18e7bd95ca4326294a64c5
4
- data.tar.gz: 2b8302f2ef6dc7aaad982c1519be8e22d08dcecd559588968789745e9af0a706
3
+ metadata.gz: 0f9c3cffdb4484e108a60fb3f34a59ddff273b1d519fc7e5da6dabbe3a56c49a
4
+ data.tar.gz: 8e2ec6f4a3a18b795e4cd9906942262766439bab0b6f309a95f57f7fe7931676
5
5
  SHA512:
6
- metadata.gz: 114c117d5945dc843efe761f5b665caaed444e452f45c1a248449c250cf56c8975b990199d29d546a2a83a9ed270330e4bba619de37ec51a691a31faadd98769
7
- data.tar.gz: 717e8720c587b4860f6e9003c448632e77b32a35a98d9016c16232af26252ef70cf431f216aa465d746b39405063fb278f4daedc575bdecabb8453d7fb2bbd69
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
@@ -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)
@@ -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?
@@ -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(' ', '&nbsp;')
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
@@ -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,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
- parent = nil
15
- records = []
16
- File.foreach(file).with_index do |line, line_num|
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
- 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
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
- 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
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
@@ -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 inspect
57
- "#<#{self.class.name}> #{to_h}"
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
- validations[name] ||= []
69
- validations[name] << validation_method
70
- end
71
-
72
- def self.definition
73
- @definition ||= {}
74
- end
75
-
76
- def self.validations
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]}&nbsp;|&nbsp</span>" +
161
+ field_html +
162
+ "<span class=\"record-type\" data-name=\"record-type\">#{human_name}</span>" +
163
+ "</div>"
78
164
  end
79
165
 
80
- class << self
81
- attr_reader :nacha_record_name
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
- 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
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
- # rubocop:disable GitlabSecurity/PublicSend
103
- field = send(field_sym)
104
- # rubocop:enable GitlabSecurity/PublicSend
105
- 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?
106
195
 
107
- ## TODO: levels of validity with 'R' and 'O' fields
108
- field.valid?
109
- end
110
- !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)
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 self.unpack_str
124
- @unpack_str ||= definition.values.collect do |d|
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 self.parse(ach_str)
142
- rec = new
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)
@@ -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.1'
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
@@ -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.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: 2025-06-01 00:00:00.000000000 Z
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.3
321
+ rubygems_version: 3.6.7
274
322
  specification_version: 4
275
323
  summary: Ruby parser for ACH files.
276
324
  test_files: []