nacha 0.1.4 → 0.1.6

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: 477c410ef1a0443262759b86291032260ad0e6f125421ab58894110895a9d087
4
- data.tar.gz: f6b132e7c97d5f38688ca32f7c893f83775cc74bc42dbc4d1b13c158d55485c2
3
+ metadata.gz: 782ae8de3e488c9404aab641bf27e71da0b9bc6d391d235dbec10fc8d750c27a
4
+ data.tar.gz: 76e485087bcdd967c437912b1318bd79f21e50c1e9ed6eb3dbd73ea3233b86ec
5
5
  SHA512:
6
- metadata.gz: 4566aa3cd1a369abbf6f80274914a730fd987f84ab06daa6a0fb7916221ea9ca1510ba254cea24d39c701ffa57937df867e687fa5cb31f904924948400ea5eae
7
- data.tar.gz: ddc39965a93e0b14b2a9517736af0c1b5dd8c7620fd2fbf4dce2a914342ccd979bb66799bc278446d795a16f9b34720e89b69f9192d6e8a794c2570f90c2a812
6
+ metadata.gz: 799751bb17453a1f28e7e0b362e7a06eb017445d5928c06e7130c15c6d7b6a6a427e2b96ecb05567fafc60107400c82e39e0c81799158f2d942af6839ba470b7
7
+ data.tar.gz: adf86d5aa5302590c53c0333e3ea7a32c5e0e3ed255da5bf8812d0d2e853173e31b8cf12a7ba1602b9c0420f1aa52eedd9c54f7698f90ba11f0a399bd787bb42
data/CHANGELOG.md ADDED
@@ -0,0 +1,81 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.6] - 2025-06-20
11
+
12
+ - Better code coverage
13
+
14
+ - fixed an issue with jruby running tests
15
+
16
+ - Bump version to 0.1.6
17
+
18
+
19
+ ## [0.1.5] - 2025-06-18
20
+
21
+ ### Fixed
22
+ - Actual reporting of field errors now in the output html.
23
+ - Change Nacha::Record::BatchControl#reserved to optional
24
+ - Change Nacha::Record::BatchHeader#settlement_date_julian to optional
25
+ - bump version to 0.1.5
26
+
27
+ ## [0.1.4] - 2025-06-14
28
+
29
+ ### Added
30
+ - Store the original input line on parsed records for easier debugging.
31
+
32
+ ### Changed
33
+ - Group metadata into a nested `metadata` object in `to_h` output for better organization.
34
+
35
+ ### Fixed
36
+ - Resolved a module loading issue.
37
+
38
+ ## [0.1.3] - 2025-06-14
39
+
40
+ ### Added
41
+ - Introduced SimpleCov for tracking code coverage.
42
+ - Added comprehensive tests for `Nacha::Record::Filler`.
43
+
44
+ ### Changed
45
+ - Improved parsing with stricter matching for numeric fields and better handling of records not 94 bytes long.
46
+ - Enhanced HTML output with better class handling.
47
+ - Refactored `Nacha::Numeric` for better clarity and added more tests.
48
+
49
+ ### Fixed
50
+ - Improved error parsing and reporting.
51
+ - Corrected memoization for `Nacha::Record.matcher`.
52
+
53
+ ## [0.1.2] - 2025-06-04
54
+
55
+ ### Added
56
+ - Introduced a command-line interface (CLI) using Thor.
57
+ - The CLI includes a `parse` command to process ACH files and output a summary to the console or generate an HTML representation.
58
+
59
+ ## [0.1.1] - 2025-05-31
60
+
61
+ ### Fixed
62
+ - Corrected an issue with `Nacha::AchDate` parsing on JRuby.
63
+ - Fixed gem file loading in Rails environments.
64
+ - Replaced deprecated `BigDecimal.new` with `BigDecimal()`.
65
+
66
+ ### Removed
67
+ - Removed `pry` as a dependency.
68
+
69
+ ## [0.1.0] - 2017-03-06
70
+
71
+ ### Added
72
+ - Initial release of the Nacha gem.
73
+ - Core functionality to define NACHA record types and their fields.
74
+ - Parser for processing NACHA files from strings.
75
+ - Ability to generate NACHA-formatted strings from record objects (`to_ach`).
76
+ - Ability to convert records to JSON (`to_json`) and Hashes (`to_h`).
77
+ - Initial implementation of field and record validations.
78
+ - Extensive test suite using RSpec and later FactoryBot.
79
+
80
+ ### Changed
81
+ - Major architectural refactoring to define records in pure Ruby classes (`nacha_field`) instead of external YAML files.
data/exe/nacha CHANGED
@@ -36,10 +36,12 @@ module Nacha
36
36
 
37
37
  private
38
38
 
39
- def output_html(file_path, ach_file)
39
+ def output_html(file_path, ach_records)
40
40
  puts html_preamble
41
41
  puts "<h1>Successfully parsed #{file_path}</h1>\n"
42
- display_child(0, ach_file.first) # Display the first record
42
+ ach_records.each do |record|
43
+ display_child(0, record)
44
+ end
43
45
  puts html_postamble
44
46
  end
45
47
 
@@ -55,17 +57,12 @@ module Nacha
55
57
  # Attempt to call a summary or to_s method if it exists,
56
58
  # otherwise inspect the record.
57
59
  return unless record
58
- level_indent = ' ' * level.to_i
59
- puts "<html>"
60
60
  puts record.to_html
61
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
62
+ record.children.each do |child_record|
63
+ display_child(level + 1, child_record)
66
64
  end
67
65
  end
68
- puts "</html>"
69
66
  end
70
67
  end
71
68
  end
@@ -9,6 +9,7 @@ class Nacha::AbaNumber
9
9
  include HasErrors
10
10
 
11
11
  def initialize(routing_number)
12
+ @errors = []
12
13
  self.routing_number = routing_number
13
14
  end
14
15
 
@@ -23,7 +24,8 @@ class Nacha::AbaNumber
23
24
 
24
25
  def routing_number=(val)
25
26
  @valid = nil
26
- @routing_number = val.strip
27
+ @errors&.clear
28
+ @routing_number = val.to_s.strip
27
29
  end
28
30
 
29
31
  def add_check_digit
@@ -35,15 +37,25 @@ class Nacha::AbaNumber
35
37
  end
36
38
 
37
39
  def valid?
38
- @valid ||= valid_routing_number_length? && valid_check_digit?
40
+ @valid ||= if valid_routing_number_length?
41
+ if @routing_number.length == 9
42
+ valid_check_digit?
43
+ else # 8 digits is valid
44
+ true
45
+ end
46
+ else
47
+ false
48
+ end
39
49
  end
40
50
 
41
51
  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
52
+ actual_length = @routing_number.length
53
+
54
+ if [9, 10].include?(actual_length)
46
55
  true
56
+ else
57
+ add_error("Routing number must be 8 or 9 digits long, but was #{actual_length} digits long.")
58
+ false
47
59
  end
48
60
  end
49
61
 
@@ -36,7 +36,7 @@ class Nacha::AchDate < Date
36
36
  # This works because `Date.new` is designed to be effectively `allocate.initialize`.
37
37
  super(year, month, day)
38
38
 
39
- rescue ArgumentError => e
39
+ rescue TypeError, ArgumentError => e
40
40
  # Catch errors that might arise from strptime or invalid date components
41
41
  raise ArgumentError, "Invalid date format for Nacha::AchDate: #{args.inspect}. Original error: #{e.message}"
42
42
  end
data/lib/nacha/field.rb CHANGED
@@ -25,6 +25,8 @@ class Nacha::Field
25
25
  @fill_character = ' '
26
26
  @json_output = [:to_s]
27
27
  @output_conversion = [:to_s]
28
+ @validated = false
29
+ @data_assigned = false
28
30
  opts.each do |k, v|
29
31
  setter = "#{k}="
30
32
  if respond_to?(setter)
@@ -38,8 +40,6 @@ class Nacha::Field
38
40
  def contents=(val)
39
41
  @contents = val
40
42
  case @contents
41
- when /\AC(.*)\z/ # Constant
42
- @data = Regexp.last_match(1)
43
43
  when /\$.*¢*/
44
44
  @data_type = Nacha::Numeric
45
45
  @justification = :rjust
@@ -71,10 +71,15 @@ class Nacha::Field
71
71
  @justification = :ljust
72
72
  @output_conversion = [:to_s]
73
73
  @fill_character = ' '
74
+ when /\AC(.*)\z/ # Constant
75
+ @data = Regexp.last_match(1)
74
76
  end
75
77
  end
76
78
 
77
79
  def data=(val)
80
+ @validated = false
81
+ @data_assigned = true
82
+ @errors = []
78
83
  @data = @data_type.new(val)
79
84
  @input_data = val
80
85
  rescue StandardError => e
@@ -96,10 +101,44 @@ class Nacha::Field
96
101
  @inclusion == 'O'
97
102
  end
98
103
 
104
+ def validate
105
+ return if @validated
106
+
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 && (mandatory? || required?) && (@input_data.nil? || @input_data.to_s.strip.empty?)
112
+ add_error("'#{human_name}' is a required field and cannot be blank.")
113
+ end
114
+
115
+ # Type-specific validations
116
+ if @data_type == Nacha::Numeric && @input_data.to_s.strip.match(/\D/)
117
+ add_error("Invalid characters in numeric field '#{human_name}'. Got '#{@input_data}'.")
118
+ end
119
+
120
+ # If data object has its own validation, run it.
121
+ if @validator && @data.is_a?(@data_type)
122
+ # The call to the validator might populate errors on the data object.
123
+ is_valid = @data.send(@validator)
124
+
125
+ # Collect any errors from the data object.
126
+ if @data.respond_to?(:errors) && @data.errors && @data.errors.any?
127
+ @data.errors.each { |e| add_error(e) }
128
+ end
129
+
130
+ # If it's not valid and we haven't collected any specific errors, add a generic one.
131
+ if !is_valid && errors.empty?
132
+ add_error("'#{human_name}' is invalid. Got '#{@input_data}'.")
133
+ end
134
+ end
135
+
136
+ @validated = true
137
+ end
138
+
99
139
  def valid?
100
- @valid = inclusion && contents && position
101
- @valid &&= @data.send(@validator) if @validator && @data
102
- @valid
140
+ validate
141
+ errors.empty?
103
142
  end
104
143
 
105
144
  def add_error(err_string)
@@ -138,7 +177,7 @@ class Nacha::Field
138
177
  end
139
178
 
140
179
  def to_html
141
- tooltip_text = "<span class=\"tooltiptext\" >#{human_name}</span>"
180
+ tooltip_text = "<span class=\"tooltiptext\" >#{human_name} #{errors.join(' ')}</span>"
142
181
  field_classes = ["nacha-field tooltip"]
143
182
  field_classes += ['mandatory'] if mandatory?
144
183
  field_classes += ['required'] if required?
data/lib/nacha/numeric.rb CHANGED
@@ -5,14 +5,12 @@ class Nacha::Numeric
5
5
  end
6
6
 
7
7
  def to_i
8
- if @value
9
- if @value.is_a?(String) && @value.match(/\A *\z/)
10
- @value # blank strings should return as blank
11
- else
12
- @value.to_i
13
- end
8
+ return 0 if @value.nil?
9
+
10
+ if @value.is_a?(String) && @value.match(/\A *\z/)
11
+ @value # blank strings should return as blank
14
12
  else
15
- self
13
+ @value.to_i
16
14
  end
17
15
  end
18
16
 
@@ -11,7 +11,7 @@ class Nacha::ParserContext
11
11
  attr_accessor :previous_record
12
12
  attr_reader :validated
13
13
 
14
- def initialize (opts = {})
14
+ def initialize(opts = {})
15
15
  @file_name = opts[:file_name] || ''
16
16
  @line_number = opts[:line_number] || 0
17
17
  @line_length = opts[:line_length] || 0
@@ -16,9 +16,9 @@ module Nacha
16
16
  nacha_field :total_credit_entry_dollar_amount_in_file, inclusion: 'M', contents: '$$$$$$$$$$¢¢', position: 52..71
17
17
  nacha_field :reserved, inclusion: 'M', contents: 'C', position: 72..94
18
18
 
19
- def initialize
20
- create_fields_from_definition
21
- end
19
+ # def initialize
20
+ # create_fields_from_definition
21
+ # end
22
22
  end
23
23
  end
24
24
  end
@@ -10,12 +10,13 @@ module Nacha
10
10
  include Validations::FieldValidations
11
11
 
12
12
  attr_accessor :children, :parent, :fields
13
- attr_reader :name, :validations, :errors
13
+ attr_reader :name, :validations, :errors, :original_input_line
14
14
  attr_accessor :line_number
15
15
 
16
16
  def initialize(opts = {})
17
17
  @children = []
18
18
  @errors = []
19
+ @original_input_line = nil
19
20
  create_fields_from_definition
20
21
  opts.each do |k, v|
21
22
  setter = "#{k}="
@@ -113,8 +114,14 @@ module Nacha
113
114
  ach_str.unpack(unpack_str).zip(rec.fields.values) do |input_data, field|
114
115
  field.data = input_data
115
116
  end
117
+ rec.original_input_line = ach_str
118
+ rec.validate
116
119
  rec
117
120
  end
121
+ end # end class methods
122
+
123
+ def original_input_line=(line)
124
+ @original_input_line = line.dup if line.is_a?(String)
118
125
  end
119
126
 
120
127
  def create_fields_from_definition
@@ -133,7 +140,13 @@ module Nacha
133
140
  end
134
141
 
135
142
  def to_h
136
- { nacha_record_type: record_type }.merge(
143
+ { nacha_record_type: record_type,
144
+ metadata: {
145
+ errors: errors,
146
+ line_number: @line_number,
147
+ original_input_line: original_input_line
148
+ }
149
+ }.merge(
137
150
  @fields.keys.map do |key|
138
151
  [key, @fields[key].to_json_output]
139
152
  end.to_h)
@@ -172,8 +185,12 @@ module Nacha
172
185
  end
173
186
 
174
187
  def validate
175
- failing_checks = self.class.definition.keys.map do |field|
176
- next true unless self.class.validations[field]
188
+ # Run field-level validations first
189
+ @fields.values.each(&:validate)
190
+
191
+ # Then run record-level validations that might depend on multiple fields
192
+ self.class.definition.keys.each do |field|
193
+ next unless self.class.validations[field]
177
194
 
178
195
  # rubocop:disable GitlabSecurity/PublicSend
179
196
  field_data = send(field)
@@ -182,22 +199,13 @@ module Nacha
182
199
  self.class.send(validation_method, field_data)
183
200
  end
184
201
  # rubocop:enable GitlabSecurity/PublicSend
185
- end.flatten
202
+ end
186
203
  end
187
204
 
188
205
  # look for invalid fields, if none, then return true
189
206
  def valid?
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?
195
-
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)
207
+ validate
208
+ errors.empty?
201
209
  end
202
210
 
203
211
  # Checks if the current transaction code represents a debit transaction.
@@ -16,7 +16,7 @@ module Nacha
16
16
  nacha_field :total_credit_entry_dollar_amount, inclusion: 'M', contents: '$$$$$$$$$$¢¢', position: 33..44
17
17
  nacha_field :company_identification, inclusion: 'R', contents: 'Alphameric', position: 45..54
18
18
  nacha_field :message_authentication_code, inclusion: 'O', contents: 'Alphameric', position: 55..73
19
- nacha_field :reserved, inclusion: 'M', contents: 'C ', position: 74..79
19
+ nacha_field :reserved, inclusion: 'O', contents: 'C ', position: 74..79
20
20
  nacha_field :originating_dfi_identification, inclusion: 'M', contents: 'TTTTAAAA', position: 80..87
21
21
  nacha_field :batch_number, inclusion: 'M', contents: 'Numeric', position: 88..94
22
22
  end
@@ -17,7 +17,7 @@ module Nacha
17
17
  nacha_field :company_entry_description, inclusion: 'M', contents: 'Alphameric', position: 54..63
18
18
  nacha_field :company_descriptive_date, inclusion: 'O', contents: 'Alphameric', position: 64..69
19
19
  nacha_field :effective_entry_date, inclusion: 'R', contents: 'YYMMDD', position: 70..75
20
- nacha_field :settlement_date_julian, inclusion: 'M', contents: 'Numeric', position: 76..78
20
+ nacha_field :settlement_date_julian, inclusion: 'O', contents: 'Numeric', position: 76..78
21
21
  nacha_field :originator_status_code, inclusion: 'M', contents: 'Alphameric', position: 79..79
22
22
  nacha_field :originating_dfi_identification, inclusion: 'M', contents: 'TTTTAAAA', position: 80..87
23
23
  nacha_field :batch_number, inclusion: 'M', contents: 'Numeric', position: 88..94
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.4'
4
+ VERSION = '0.1.6'
5
5
  end
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
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - David H. Wilkins
@@ -220,6 +220,7 @@ files:
220
220
  - ".ruby-gemset"
221
221
  - ".ruby-version"
222
222
  - ".travis.yml"
223
+ - CHANGELOG.md
223
224
  - Gemfile
224
225
  - Guardfile
225
226
  - LICENSE.txt