nacha 0.1.10 → 0.1.12

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.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -2
  3. data/.ruby-version +1 -1
  4. data/CHANGELOG.md +17 -0
  5. data/exe/nacha +51 -20
  6. data/lib/nacha/aba_number.rb +17 -14
  7. data/lib/nacha/ach_date.rb +15 -8
  8. data/lib/nacha/field.rb +69 -58
  9. data/lib/nacha/has_errors.rb +12 -8
  10. data/lib/nacha/numeric.rb +13 -10
  11. data/lib/nacha/parser.rb +22 -27
  12. data/lib/nacha/parser_context.rb +4 -9
  13. data/lib/nacha/record/ack_entry_detail.rb +15 -8
  14. data/lib/nacha/record/adv_batch_control.rb +12 -7
  15. data/lib/nacha/record/adv_entry_detail.rb +3 -2
  16. data/lib/nacha/record/adv_file_control.rb +9 -5
  17. data/lib/nacha/record/adv_file_header.rb +11 -6
  18. data/lib/nacha/record/arc_entry_detail.rb +2 -2
  19. data/lib/nacha/record/base.rb +121 -106
  20. data/lib/nacha/record/batch_control.rb +13 -7
  21. data/lib/nacha/record/batch_header.rb +20 -11
  22. data/lib/nacha/record/batch_header_record_type.rb +1 -1
  23. data/lib/nacha/record/boc_entry_detail.rb +3 -2
  24. data/lib/nacha/record/ccd_addenda.rb +2 -2
  25. data/lib/nacha/record/ccd_entry_detail.rb +3 -2
  26. data/lib/nacha/record/cie_addenda.rb +2 -2
  27. data/lib/nacha/record/cie_entry_detail.rb +5 -3
  28. data/lib/nacha/record/ctx_addenda.rb +2 -2
  29. data/lib/nacha/record/ctx_corporate_entry_detail.rb +2 -2
  30. data/lib/nacha/record/dne_addenda.rb +2 -2
  31. data/lib/nacha/record/dne_entry_detail.rb +6 -4
  32. data/lib/nacha/record/enr_addenda.rb +3 -2
  33. data/lib/nacha/record/enr_entry_detail.rb +4 -3
  34. data/lib/nacha/record/fifth_iat_addenda.rb +8 -4
  35. data/lib/nacha/record/file_control.rb +9 -5
  36. data/lib/nacha/record/file_control_record_type.rb +1 -1
  37. data/lib/nacha/record/file_header.rb +12 -8
  38. data/lib/nacha/record/file_header_record_type.rb +1 -1
  39. data/lib/nacha/record/filler.rb +3 -3
  40. data/lib/nacha/record/filler_record_type.rb +3 -1
  41. data/lib/nacha/record/first_iat_addenda.rb +3 -2
  42. data/lib/nacha/record/fourth_iat_addenda.rb +7 -4
  43. data/lib/nacha/record/iat_batch_header.rb +5 -3
  44. data/lib/nacha/record/iat_entry_detail.rb +7 -4
  45. data/lib/nacha/record/iat_foreign_coorespondent_bank_information_addenda.rb +10 -6
  46. data/lib/nacha/record/iat_remittance_information_addenda.rb +3 -2
  47. data/lib/nacha/record/mte_addenda.rb +4 -3
  48. data/lib/nacha/record/mte_entry_detail.rb +5 -3
  49. data/lib/nacha/record/pop_entry_detail.rb +3 -2
  50. data/lib/nacha/record/pos_addenda.rb +6 -3
  51. data/lib/nacha/record/pos_entry_detail.rb +5 -3
  52. data/lib/nacha/record/ppd_addenda.rb +3 -2
  53. data/lib/nacha/record/ppd_entry_detail.rb +5 -3
  54. data/lib/nacha/record/rck_entry_detail.rb +3 -2
  55. data/lib/nacha/record/second_iat_addenda.rb +3 -2
  56. data/lib/nacha/record/seventh_iat_addenda.rb +3 -2
  57. data/lib/nacha/record/shr_addenda.rb +5 -3
  58. data/lib/nacha/record/shr_entry_detail.rb +3 -2
  59. data/lib/nacha/record/sixth_iat_addenda.rb +5 -3
  60. data/lib/nacha/record/tel_entry_detail.rb +5 -3
  61. data/lib/nacha/record/third_iat_addenda.rb +3 -2
  62. data/lib/nacha/record/trc_entry_detail.rb +3 -2
  63. data/lib/nacha/record/trx_addenda.rb +3 -2
  64. data/lib/nacha/record/trx_entry_detail.rb +5 -3
  65. data/lib/nacha/record/validations/field_validations.rb +26 -14
  66. data/lib/nacha/record/validations/record_validations.rb +2 -1
  67. data/lib/nacha/record/web_addenda.rb +3 -2
  68. data/lib/nacha/record/web_entry_detail.rb +5 -3
  69. data/lib/nacha/record/xck_entry_detail.rb +3 -2
  70. data/lib/nacha/version.rb +4 -1
  71. data/lib/nacha.rb +21 -14
  72. data/nacha.gemspec +13 -16
  73. metadata +22 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6d8625f3af750aa7ba6e526909ad823f6020b761ab740616e82650f856585a13
4
- data.tar.gz: 459d7359527c582eddf7d46770b2527896d465db86be48a1f6a994bc8d15471d
3
+ metadata.gz: 737ec495f2219167b7ef5ec5a959a6b205a4cf551eba2fdee6b5d3f0324b0735
4
+ data.tar.gz: 67f479ed3d4f1affb7d7a0f3fb92168bc2554a84b33cca2c3108d884a36e3989
5
5
  SHA512:
6
- metadata.gz: fb31a6b021d3a7b7752f079651bfc5d2f78d5623883ab91201e76853273c3002c1d4a0423d971d5c75a2c38b03faee3e31d64d7cf82cb28f632c4c4174a6526b
7
- data.tar.gz: 2aaf5af05547d0e0f10afb1f2a310e70b69e11c359979df5d238389c7800ab3f76ff163a5f35275162809f44b923603c2ed69658c96fe11e7f86eb8cbdf204cf
6
+ metadata.gz: 3fbae7e0193e68fa8fda30e32ff8272c4380556352fd6b64d3fdd6eb806e974ac147952a033603810f712f06b5437207a7a0d6a7d84ae4abe435c87314716329
7
+ data.tar.gz: 666b409414698a8502a69d02dca7cdc35f929cda66c66a6ef68430701d252f4640ed8aaaeeec1ba0064c5ff49a94befe189a69757fb5903142eede3a706dcfbe
data/.rubocop.yml CHANGED
@@ -8,6 +8,7 @@ AllCops:
8
8
  - 'tmp/**/*'
9
9
  - 'bin/**/*'
10
10
  CacheRootDirectory: tmp
11
+ SuggestExtensions: false
11
12
 
12
13
  # This cop checks whether some constant value isn't a
13
14
  # mutable literal (e.g. array or hash).
@@ -25,8 +26,8 @@ Style/FrozenStringLiteralComment:
25
26
  - 'Rakefile'
26
27
  - 'spec/**/*'
27
28
 
28
- Metrics/LineLength:
29
- Max: 92
29
+ Layout/LineLength:
30
+ Max: 108
30
31
 
31
32
  Naming/FileName:
32
33
  ExpectMatchingDefinition: true
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.4.4
1
+ 3.2.8
data/CHANGELOG.md CHANGED
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.12] - 2025-07-08
11
+
12
+ - Fixed a bug with parsing files with misshapen lines. Now it
13
+ _should_ handle lines that are shorter than 94 characters _and_ have
14
+ (CR|CRLF|LF) as the terminating character.
15
+
16
+
17
+
18
+ ## [0.1.11] - 2025-07-05
19
+
20
+ ### Output formatting options
21
+ - use -f [html|json|ach] or --format [html|json|ach] to change
22
+
23
+ - use -o filename or --output filename to specify a file to output
24
+
25
+ - Aider and I fixed a bunch of rubocop offenses and refactored some code
26
+
10
27
  ## [0.1.10] - 2025-07-01
11
28
 
12
29
  - Added ability to get a list of possible record types and
data/exe/nacha CHANGED
@@ -1,29 +1,42 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
5
+ require 'byebug'
3
6
  require 'thor'
7
+ require 'json'
4
8
  require 'nacha' # Assuming this loads the Nacha gem
5
9
 
6
10
  module Nacha
7
11
  class CLI < Thor
8
12
  TEMPLATES_DIR = File.join(Gem::Specification.find_by_name("nacha").gem_dir,
9
- "templates").freeze
13
+ "templates").freeze
10
14
 
11
15
  HTML_PREAMBLE_FILE = File.join(TEMPLATES_DIR, "html_preamble.html")
12
16
  HTML_POSTAMBLE_FILE = File.join(TEMPLATES_DIR, "html_postamble.html")
13
17
 
14
18
  desc "parse FILE", "Parse an ACH file"
19
+ option :output_file, aliases: "-o"
20
+ option :format, aliases: "-f", default: "html",
21
+ desc: "Output format (html, json, or ach)", enum: %w[html json ach]
15
22
  def parse(file_path)
16
23
  unless File.exist?(file_path)
17
24
  puts "Error: File not found at #{file_path}"
18
25
  exit 1
19
26
  end
20
27
 
21
- ach_file = [Nacha.parse(File.open(file_path)).first] # Use Nacha.parse
28
+ ach_file = Nacha.parse(File.open(file_path))
22
29
 
23
30
  # TODO: Determine a user-friendly way to output the parsed data.
24
31
  # For now, let's print the records.
25
32
  if ach_file && ach_file.is_a?(Array) && !ach_file.empty?
26
- output_html(file_path, ach_file)
33
+ if options[:output_file]
34
+ File.open(options[:output_file], "w") do |file|
35
+ write_output(ach_file, file)
36
+ end
37
+ else
38
+ write_output(ach_file, $stdout)
39
+ end
27
40
  else
28
41
  puts "Could not parse the file or the file was empty."
29
42
  end
@@ -35,13 +48,43 @@ module Nacha
35
48
 
36
49
  private
37
50
 
38
- def output_html(file_path, ach_records)
39
- puts html_preamble
40
- puts "<h1>Successfully parsed #{file_path}</h1>\n"
51
+ def write_output(ach_records, io)
52
+ case options[:format]
53
+ when 'html'
54
+ output_html(ach_records, io)
55
+ when 'json'
56
+ output_json(ach_records, io)
57
+ when 'ach'
58
+ output_ach(ach_records, io)
59
+ end
60
+ end
61
+
62
+ def output_json(ach_records, io)
63
+ io.puts JSON.pretty_generate(ach_records.map(&:to_h))
64
+ # io.puts JSON.pretty_generate(json_output)
65
+ end
66
+
67
+ def record_to_h(record)
68
+ {
69
+ record.class.name.split('::').last => {
70
+ fields: record.fields.transform_values(&:to_s),
71
+ children: record.children.map { |child| record_to_h(child) }
72
+ }
73
+ }
74
+ end
75
+
76
+ def output_ach(ach_records, io)
41
77
  ach_records.each do |record|
42
- display_child(0, record)
78
+ io.puts record.to_ach
43
79
  end
44
- puts html_postamble
80
+ end
81
+
82
+ def output_html(ach_records, io)
83
+ io.puts html_preamble
84
+ ach_records.each do |record|
85
+ io.puts record.to_html
86
+ end
87
+ io.puts html_postamble
45
88
  end
46
89
 
47
90
  def html_preamble
@@ -51,18 +94,6 @@ module Nacha
51
94
  def html_postamble
52
95
  @html_postamble ||= File.read(HTML_POSTAMBLE_FILE)
53
96
  end
54
-
55
- def display_child(level, record)
56
- # Attempt to call a summary or to_s method if it exists,
57
- # otherwise inspect the record.
58
- return unless record
59
- puts record.to_html
60
- if record.respond_to?(:children) && record.children.any?
61
- record.children.each do |child_record|
62
- display_child(level + 1, child_record)
63
- end
64
- end
65
- end
66
97
  end
67
98
  end
68
99
 
@@ -3,10 +3,9 @@
3
3
  require "nacha/has_errors"
4
4
 
5
5
  class Nacha::AbaNumber
6
- attr_reader :routing_number
7
- attr_reader :aba_number
6
+ attr_reader :routing_number, :aba_number
8
7
 
9
- include HasErrors
8
+ include Nacha::HasErrors
10
9
 
11
10
  def initialize(routing_number)
12
11
  @errors = []
@@ -33,19 +32,21 @@ class Nacha::AbaNumber
33
32
  end
34
33
 
35
34
  def check_digit
36
- @routing_number.chars[8] if @routing_number.length == 9 && compute_check_digit == @routing_number.chars[8]
35
+ return unless @routing_number.length == 9 && compute_check_digit == @routing_number[8]
36
+
37
+ @routing_number[8]
37
38
  end
38
39
 
39
40
  def valid?
40
41
  @valid ||= if valid_routing_number_length?
41
- if @routing_number.length == 9
42
- valid_check_digit?
43
- else # 8 digits is valid
44
- true
42
+ if @routing_number.length == 9
43
+ valid_check_digit?
44
+ else # 8 digits is valid
45
+ true
46
+ end
47
+ else
48
+ false
45
49
  end
46
- else
47
- false
48
- end
49
50
  end
50
51
 
51
52
  def valid_routing_number_length?
@@ -54,14 +55,16 @@ class Nacha::AbaNumber
54
55
  if [9, 10].include?(actual_length)
55
56
  true
56
57
  else
57
- add_error("Routing number must be 8 or 9 digits long, but was #{actual_length} digits long.")
58
+ add_error("Routing number must be 8 or 9 digits long, but was " \
59
+ "#{actual_length} digits long.")
58
60
  false
59
61
  end
60
62
  end
61
63
 
62
64
  def valid_check_digit?
63
- if compute_check_digit != @routing_number.chars[8]
64
- add_error("Incorrect Check Digit \"#{@routing_number.chars[8]}\" should be \"#{ compute_check_digit }\" ")
65
+ if compute_check_digit != @routing_number[8]
66
+ add_error("Incorrect Check Digit \"#{@routing_number[8]}\" should be " \
67
+ "\"#{compute_check_digit}\"")
65
68
  false
66
69
  else
67
70
  true
@@ -12,22 +12,28 @@ class Nacha::AchDate < Date
12
12
  date_str = args[0]
13
13
  # Use Date.strptime to parse the string into a temporary Date object
14
14
  temp_date = Date.strptime(date_str, '%y%m%d')
15
- year, month, day = temp_date.year, temp_date.month, temp_date.day
15
+ year = temp_date.year
16
+ month = temp_date.month
17
+ day = temp_date.day
16
18
  when Date
17
19
  original_date = args[0]
18
- year, month, day = original_date.year, original_date.month, original_date.day
20
+ year = original_date.year
21
+ month = original_date.month
22
+ day = original_date.day
19
23
  when Integer # If it's a year integer, assume (year, month, day) or single Julian day
20
24
  # If 3 arguments (year, month, day) are provided like Date.new(2023, 10, 26)
21
- if args.length == 3 && args.all? { |arg| arg.is_a?(Integer) }
22
- year, month, day = args[0], args[1], args[2]
23
- else
25
+ unless args.length == 3 && args.all?(Integer)
24
26
  # Fallback for other Date.new arguments like (jd) - let super handle directly
25
- return super(*args) # IMPORTANT: Call super to create the instance
27
+ return super # IMPORTANT: Call super to create the instance
26
28
  end
29
+
30
+ year = args[0]
31
+ month = args[1]
32
+ day = args[2]
27
33
  else
28
34
  # If it's none of the above, pass arguments directly to Date.new.
29
35
  # This handles cases like Date.new(2459918) (Julian day) or other Date constructors.
30
- return super(*args)
36
+ return super
31
37
  end
32
38
 
33
39
  # If year, month, day were successfully parsed, create a Nacha::AchDate instance
@@ -38,7 +44,8 @@ class Nacha::AchDate < Date
38
44
 
39
45
  rescue TypeError, ArgumentError => e
40
46
  # Catch errors that might arise from strptime or invalid date components
41
- raise ArgumentError, "Invalid date format for Nacha::AchDate: #{args.inspect}. Original error: #{e.message}"
47
+ raise ArgumentError, "Invalid date format for Nacha::AchDate: #{args.inspect}. " \
48
+ "Original error: #{e.message}"
42
49
  end
43
50
 
44
51
  def to_s
data/lib/nacha/field.rb CHANGED
@@ -7,15 +7,17 @@ require 'nacha/aba_number'
7
7
  require 'nacha/ach_date'
8
8
 
9
9
  class Nacha::Field
10
- attr_accessor :inclusion, :position
11
- attr_accessor :name, :errors
12
- attr_reader :contents, :data, :input_data
13
- attr_reader :data_type
14
- attr_reader :validator
15
- attr_reader :justification
16
- attr_reader :fill_character
17
- attr_reader :output_conversion
18
- attr_reader :json_output
10
+ attr_accessor :inclusion, :position, :name, :errors
11
+ attr_reader :contents, :data, :input_data, :data_type, :validator,
12
+ :justification, :fill_character, :output_conversion, :json_output
13
+
14
+ def self.unpack_str(definition = {})
15
+ if definition[:contents].match?(/(Numeric|\$+\u00a2\u00a2)/)
16
+ 'a'
17
+ else
18
+ 'A'
19
+ end + definition[:position].size.to_s
20
+ end
19
21
 
20
22
  def initialize(opts = {})
21
23
  @data_type = String
@@ -29,9 +31,9 @@ class Nacha::Field
29
31
  @data_assigned = false
30
32
  opts.each do |k, v|
31
33
  setter = "#{k}="
32
- if respond_to?(setter)
33
- send(setter, v) unless v.nil?
34
- end
34
+ # rubocop:disable GitlabSecurity/PublicSend
35
+ public_send(setter, v) if respond_to?(setter) && !v.nil?
36
+ # rubocop:enable GitlabSecurity/PublicSend
35
37
  end
36
38
  end
37
39
 
@@ -43,7 +45,7 @@ class Nacha::Field
43
45
  when /\$.*¢*/
44
46
  @data_type = Nacha::Numeric
45
47
  @justification = :rjust
46
- cents = 10 ** (@contents.count('¢'))
48
+ cents = 10**@contents.count('¢')
47
49
  @json_output = [[:to_i], [:/, cents]]
48
50
  @output_conversion = [:to_i]
49
51
  @fill_character = '0'
@@ -104,36 +106,10 @@ class Nacha::Field
104
106
  def validate
105
107
  return if @validated
106
108
 
107
- add_error("'inclusion' must be present for a field definition.") unless @inclusion
108
- add_error("'position' must be present for a field definition.") unless @position
109
- add_error("'contents' must be present for a field definition.") unless @contents
110
-
111
- if @data_assigned &&
112
- (mandatory? || required?) &&
113
- ((@input_data.nil? || @input_data.to_s.empty?) && @contents !~ /\AC( *)\z/)
114
- add_error("'#{human_name}' is a required field and cannot be blank.")
115
- end
116
-
117
- # Type-specific validations
118
- if @data_type == Nacha::Numeric && @input_data.to_s.strip.match(/\D/)
119
- add_error("Invalid characters in numeric field '#{human_name}'. Got '#{@input_data}'.")
120
- end
121
-
122
- # If data object has its own validation, run it.
123
- if @validator && @data.is_a?(@data_type)
124
- # The call to the validator might populate errors on the data object.
125
- is_valid = @data.send(@validator)
126
-
127
- # Collect any errors from the data object.
128
- if @data.respond_to?(:errors) && @data.errors && @data.errors.any?
129
- @data.errors.each { |e| add_error(e) }
130
- end
131
-
132
- # If it's not valid and we haven't collected any specific errors, add a generic one.
133
- if !is_valid && errors.empty?
134
- add_error("'#{human_name}' is invalid. Got '#{@input_data}'.")
135
- end
136
- end
109
+ validate_definition_attributes
110
+ validate_data_presence
111
+ validate_numeric_format
112
+ run_custom_validator
137
113
 
138
114
  @validated = true
139
115
  end
@@ -147,26 +123,22 @@ class Nacha::Field
147
123
  errors << err_string
148
124
  end
149
125
 
150
- def self.unpack_str(definition = {})
151
- if definition[:contents] =~ /(Numeric|\$+\u00a2\u00a2)/
152
- 'a'
153
- else
154
- 'A'
155
- end + definition[:position].size.to_s
156
- end
157
-
158
126
  def to_ach
159
127
  str = to_s
160
128
  fill_char = @fill_character
161
129
  fill_char = ' ' unless str
162
130
  str ||= ''
163
- str.send(justification, position.count, fill_char)
131
+ # rubocop:disable GitlabSecurity/PublicSend
132
+ str.public_send(justification, position.count, fill_char)
133
+ # rubocop:enable GitlabSecurity/PublicSend
164
134
  end
165
135
 
166
136
  def to_json_output
167
137
  if @json_output
168
- @json_output.reduce(@data) do |output, operation|
169
- output = output.send(*operation) if output
138
+ @json_output.reduce(@data) do |memo, operation|
139
+ # rubocop:disable GitlabSecurity/PublicSend
140
+ memo&.public_send(*operation)
141
+ # rubocop:enable GitlabSecurity/PublicSend
170
142
  end
171
143
  else
172
144
  to_s
@@ -187,16 +159,55 @@ class Nacha::Field
187
159
  field_classes += ['error'] if errors.any?
188
160
 
189
161
  ach_string = to_ach.gsub(' ', '&nbsp;')
190
- "<span data-field-name=\"#{@name}\" contentEditable=true class=\"#{field_classes.join(' ')}\" data-name=\"#{@name}\">#{ach_string}" +
191
- tooltip_text.to_s +
192
- "</span>"
162
+ "<span data-field-name=\"#{@name}\" contentEditable=true " \
163
+ "class=\"#{field_classes.join(' ')}\" data-name=\"#{@name}\">" \
164
+ "#{ach_string}#{tooltip_text}</span>"
193
165
  end
194
166
 
195
167
  def to_s
196
- @data.send(*output_conversion).to_s
168
+ # rubocop:disable GitlabSecurity/PublicSend
169
+ @data.public_send(*output_conversion).to_s
170
+ # rubocop:enable GitlabSecurity/PublicSend
197
171
  end
198
172
 
199
173
  def raw
200
174
  @data
201
175
  end
176
+
177
+ private
178
+
179
+ def validate_definition_attributes
180
+ add_error("'inclusion' must be present for a field definition.") unless @inclusion
181
+ add_error("'position' must be present for a field definition.") unless @position
182
+ add_error("'contents' must be present for a field definition.") unless @contents
183
+ end
184
+
185
+ def validate_data_presence
186
+ return unless @data_assigned && (mandatory? || required?)
187
+ return unless @input_data.nil? || @input_data.to_s.empty?
188
+ return if @contents.match?(/\AC( *)\z/)
189
+
190
+ add_error("'#{human_name}' is a required field and cannot be blank.")
191
+ end
192
+
193
+ def validate_numeric_format
194
+ return unless @data_type == Nacha::Numeric && @input_data.to_s.strip.match(/\D/)
195
+
196
+ add_error("Invalid characters in numeric field '#{human_name}'. " \
197
+ "Got '#{@input_data}'.")
198
+ end
199
+
200
+ def run_custom_validator
201
+ return unless @validator && @data.is_a?(@data_type)
202
+
203
+ # rubocop:disable GitlabSecurity/PublicSend
204
+ is_valid = @data.public_send(@validator)
205
+ # rubocop:enable GitlabSecurity/PublicSend
206
+
207
+ @data.errors.each { |e| add_error(e) } if @data.respond_to?(:errors) && @data.errors&.any?
208
+
209
+ return if is_valid || errors.any?
210
+
211
+ add_error("'#{human_name}' is invalid. Got '#{@input_data}'.")
212
+ end
202
213
  end
@@ -1,12 +1,16 @@
1
- module HasErrors
2
- attr_reader :errors
1
+ # frozen_string_literal: true
3
2
 
4
- def add_error(message)
5
- @errors ||= []
6
- @errors << message
7
- end
3
+ module Nacha
4
+ module HasErrors
5
+ attr_reader :errors
6
+
7
+ def add_error(message)
8
+ @errors ||= []
9
+ @errors << message
10
+ end
8
11
 
9
- def has_errors?
10
- !@errors.nil? && !@errors.empty?
12
+ def has_errors?
13
+ !@errors.nil? && !@errors.empty?
14
+ end
11
15
  end
12
16
  end
data/lib/nacha/numeric.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  class Nacha::Numeric
3
- def initialize val = nil
4
+ def initialize(val = nil)
4
5
  self.value = val
5
6
  end
6
7
 
@@ -17,14 +18,14 @@ class Nacha::Numeric
17
18
  def value=(val)
18
19
  if val.is_a?(String)
19
20
  @value = val.dup
20
- if(val.strip.length > 0)
21
+ if !val.strip.empty?
21
22
  @op_value = BigDecimal(val.strip)
22
23
  @value = val.dup
23
24
  else
24
- @op_value = BigDecimal(0)
25
+ @op_value = BigDecimal('0')
25
26
  end
26
27
  elsif val.nil?
27
- @value = BigDecimal(0)
28
+ @value = BigDecimal('0')
28
29
  @op_value = @value
29
30
  else
30
31
  @value = BigDecimal(val)
@@ -36,8 +37,8 @@ class Nacha::Numeric
36
37
  @value ? @value.to_i.to_s : nil
37
38
  end
38
39
 
39
- def respond_to_missing?(method_name, include_private = false)
40
- @op_value.respond_to? method_name
40
+ def respond_to_missing?(method_name, _include_private = false)
41
+ @op_value.respond_to?(method_name)
41
42
  end
42
43
 
43
44
  # @op_value is the value for operations. @value may still be a string
@@ -45,20 +46,22 @@ class Nacha::Numeric
45
46
  # should be checked to see if the operation is valid for it, not
46
47
  # necessarily the potentially string @value
47
48
  def method_missing(method_name, *args, &block)
48
- if @op_value.respond_to? method_name
49
+ if @op_value.respond_to?(method_name)
49
50
  old_op_value = @op_value.dup
50
- if /!\z/.match?(method_name.to_s)
51
- # rubocop:disable GitlabSecurity/PublicSend
51
+ # rubocop:disable GitlabSecurity/PublicSend
52
+ if method_name.to_s.end_with?('!')
52
53
  @op_value.send(method_name, *args, &block)
53
54
  return_value = @op_value
54
55
  else
55
56
  return_value = @op_value.send(method_name, *args, &block)
56
- # rubocop:enable GitlabSecurity/PublicSend
57
57
  end
58
+ # rubocop:enable GitlabSecurity/PublicSend
59
+
58
60
  if old_op_value != return_value
59
61
  @value = return_value
60
62
  @op_value = return_value
61
63
  end
64
+
62
65
  @value
63
66
  else
64
67
  super
data/lib/nacha/parser.rb CHANGED
@@ -1,13 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'byebug'
3
4
  require 'nacha'
4
5
  require 'nacha/parser_context'
5
6
 
6
7
  # Nacha Parser - deal with figuring out what record type a line is
7
8
  class Nacha::Parser
8
- DEFAULT_RECORD_TYPES = ['Nacha::Record::AdvFileHeader',
9
- 'Nacha::Record::FileHeader',
10
- 'Nacha::Record::Filler'].freeze
9
+ DEFAULT_RECORD_TYPES = [
10
+ 'Nacha::Record::FileHeader',
11
+ 'Nacha::Record::AdvFileHeader',
12
+ 'Nacha::Record::Filler'
13
+ ].freeze
11
14
 
12
15
  attr_reader :context
13
16
 
@@ -16,31 +19,25 @@ class Nacha::Parser
16
19
  end
17
20
 
18
21
  def parse_file(file)
19
- @context.parser_started_at = Time.now
22
+ @context.parser_started_at = Time.now.utc
20
23
  @context.file_name = file
21
24
  parse_string(file.read)
22
25
  end
23
26
 
24
27
  def detect_possible_record_types(line)
25
- Nacha.ach_record_types.map do |record_type|
26
- record_type if record_type.matcher =~ line
27
- end.compact
28
- end
29
-
30
- def parse_line(line)
31
- record_types = detect_possible_record_types(line)
32
-
33
- records = record_types.map do |record_type|
34
-
28
+ Nacha.ach_record_types.filter_map do |record_type|
29
+ record_type if record_type.matcher.match?(line)
35
30
  end
36
31
  end
37
32
 
38
33
  def parse_string(str)
39
34
  line_num = -1
40
35
  records = []
41
- @context.parser_started_at ||= Time.now
42
- str.scan(/(.{94}|(\A[^\n]+))/).each do |line|
43
- line = line.first.strip
36
+ @context.parser_started_at ||= Time.now.utc
37
+ str.scan(/(.{0,94})[\r\n]*/).each do |line|
38
+ line = line.compact.first.strip
39
+ next if line.empty? || line.start_with?('#') # Skip empty lines and comments
40
+
44
41
  line_num += 1
45
42
  @context.line_number = line_num
46
43
  @context.line_length = line.length
@@ -54,9 +51,11 @@ class Nacha::Parser
54
51
  parent = previous
55
52
 
56
53
  record_types = valid_record_types(parent)
54
+
57
55
  while record_types
58
56
  record = parse_first_by_types(line, record_types)
59
57
  break if record || !parent
58
+
60
59
  record.validate if record
61
60
  parent = parent.parent
62
61
  record_types = valid_record_types(parent)
@@ -80,20 +79,16 @@ class Nacha::Parser
80
79
  end
81
80
 
82
81
  def parse_first_by_types(line, record_types)
83
- record_types.detect do |rt|
82
+ record_types.lazy.filter_map do |rt|
84
83
  record_type = rt.is_a?(Class) ? rt : Object.const_get(rt)
85
- record = nil
86
- record = record_type.parse(line) if record_type.matcher =~ line
87
- return record if record
88
- end
84
+ record_type.parse(line) if record_type.matcher.match?(line)
85
+ end.first
89
86
  end
90
87
 
91
88
  def parse_all_by_types(line, record_types)
92
- record_types.map do |rt|
89
+ record_types.filter_map do |rt|
93
90
  record_type = rt.is_a?(Class) ? rt : Object.const_get(rt)
94
- record = nil
95
- record = record_type.parse(line) if record_type.matcher =~ line
96
- record
97
- end.compact
91
+ record_type.parse(line) if record_type.matcher.match?(line)
92
+ end
98
93
  end
99
94
  end