nacha 0.1.4 → 0.1.5

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: 0c263ac1b849fc88d62de1fa8e01fc3e7afbe5277c9ec96bb8317f00a587ce46
4
+ data.tar.gz: b194ada2fd9a5075279e6dd316f34dfec6ba263554920f2d72460174cd9e938c
5
5
  SHA512:
6
- metadata.gz: 4566aa3cd1a369abbf6f80274914a730fd987f84ab06daa6a0fb7916221ea9ca1510ba254cea24d39c701ffa57937df867e687fa5cb31f904924948400ea5eae
7
- data.tar.gz: ddc39965a93e0b14b2a9517736af0c1b5dd8c7620fd2fbf4dce2a914342ccd979bb66799bc278446d795a16f9b34720e89b69f9192d6e8a794c2570f90c2a812
6
+ metadata.gz: 48818fd3a324f8def81109f5306331c864564d32502a1cf0e98cf2656cf966eab29a7ce6442c176b7c493570fa54c1119e181b7d2a917170a71fd9df1d17299f
7
+ data.tar.gz: ea4ad06ae76ff311a856c3c24fb448c9f190d51806d039269984b7bf66fed21965b0e071b2c9864a19dc63da6a36ecd5bc8d9204db40a6d1ec470ac9a582f669
data/CHANGELOG.md ADDED
@@ -0,0 +1,72 @@
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.5] - 2025-06-18
11
+
12
+ ### Fixed
13
+ - Actual reporting of field errors now in the output html.
14
+ - Change Nacha::Record::BatchControl#reserved to optional
15
+ - Change Nacha::Record::BatchHeader#settlement_date_julian to optional
16
+ - bump version to 0.1.5
17
+
18
+ ## [0.1.4] - 2025-06-14
19
+
20
+ ### Added
21
+ - Store the original input line on parsed records for easier debugging.
22
+
23
+ ### Changed
24
+ - Group metadata into a nested `metadata` object in `to_h` output for better organization.
25
+
26
+ ### Fixed
27
+ - Resolved a module loading issue.
28
+
29
+ ## [0.1.3] - 2025-06-14
30
+
31
+ ### Added
32
+ - Introduced SimpleCov for tracking code coverage.
33
+ - Added comprehensive tests for `Nacha::Record::Filler`.
34
+
35
+ ### Changed
36
+ - Improved parsing with stricter matching for numeric fields and better handling of records not 94 bytes long.
37
+ - Enhanced HTML output with better class handling.
38
+ - Refactored `Nacha::Numeric` for better clarity and added more tests.
39
+
40
+ ### Fixed
41
+ - Improved error parsing and reporting.
42
+ - Corrected memoization for `Nacha::Record.matcher`.
43
+
44
+ ## [0.1.2] - 2025-06-04
45
+
46
+ ### Added
47
+ - Introduced a command-line interface (CLI) using Thor.
48
+ - The CLI includes a `parse` command to process ACH files and output a summary to the console or generate an HTML representation.
49
+
50
+ ## [0.1.1] - 2025-05-31
51
+
52
+ ### Fixed
53
+ - Corrected an issue with `Nacha::AchDate` parsing on JRuby.
54
+ - Fixed gem file loading in Rails environments.
55
+ - Replaced deprecated `BigDecimal.new` with `BigDecimal()`.
56
+
57
+ ### Removed
58
+ - Removed `pry` as a dependency.
59
+
60
+ ## [0.1.0] - 2017-03-06
61
+
62
+ ### Added
63
+ - Initial release of the Nacha gem.
64
+ - Core functionality to define NACHA record types and their fields.
65
+ - Parser for processing NACHA files from strings.
66
+ - Ability to generate NACHA-formatted strings from record objects (`to_ach`).
67
+ - Ability to convert records to JSON (`to_json`) and Hashes (`to_h`).
68
+ - Initial implementation of field and record validations.
69
+ - Extensive test suite using RSpec and later FactoryBot.
70
+
71
+ ### Changed
72
+ - 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
 
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?
@@ -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
@@ -3,19 +3,20 @@
3
3
  require 'json'
4
4
  require 'nacha/field'
5
5
  require 'nacha/record/validations/field_validations'
6
-
6
+ require 'byebug'
7
7
  module Nacha
8
8
  module Record
9
9
  class Base
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.5'
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.5
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