nacha 0.1.12 → 0.1.14

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: 737ec495f2219167b7ef5ec5a959a6b205a4cf551eba2fdee6b5d3f0324b0735
4
- data.tar.gz: 67f479ed3d4f1affb7d7a0f3fb92168bc2554a84b33cca2c3108d884a36e3989
3
+ metadata.gz: 986181f9dfa47368890ac283705b188bfef4733adff641590cc889f491cf5ed2
4
+ data.tar.gz: 1aff3dd6aedd49cf8f1e9d2562131b28832452fd0332a71429f49955aa19e858
5
5
  SHA512:
6
- metadata.gz: 3fbae7e0193e68fa8fda30e32ff8272c4380556352fd6b64d3fdd6eb806e974ac147952a033603810f712f06b5437207a7a0d6a7d84ae4abe435c87314716329
7
- data.tar.gz: 666b409414698a8502a69d02dca7cdc35f929cda66c66a6ef68430701d252f4640ed8aaaeeec1ba0064c5ff49a94befe189a69757fb5903142eede3a706dcfbe
6
+ metadata.gz: f743d90e6aba9464d81f04a9d416ac6fa13a947e2a5f42c9727a0ebda1f484b55ebdf335a0c2daf857a4bb353d079a8f853b8f9af73c0b65abd1b2d6d8802bf2
7
+ data.tar.gz: 23259ac90baaf2e6f0a31b41a5e66ffe7a952d2b42a99ca81945a90ea065ed1a1bbe2d9658403574353750dc985f57509e505b281d91a89696e81a78b43dc21a
@@ -0,0 +1,63 @@
1
+ name: CI
2
+
3
+ on: [push]
4
+
5
+ jobs:
6
+ test:
7
+ runs-on: ubuntu-latest
8
+ strategy:
9
+ fail-fast: false
10
+ matrix:
11
+ ruby: ['.ruby-version', 'jruby-head']
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - name: Set up Ruby
15
+ uses: ruby/setup-ruby@v1
16
+ with:
17
+ ruby-version: ${{ matrix.ruby }}
18
+ bundler-cache: true
19
+ - name: Run RSpec
20
+ run: bundle exec rspec
21
+ - name: Upload coverage to Coveralls
22
+ uses: coverallsapp/github-action@v2
23
+ with:
24
+ github-token: ${{ secrets.GITHUB_TOKEN }}
25
+ parallel: true
26
+ flag-name: ${{ matrix.ruby }}
27
+
28
+ coveralls_finish:
29
+ needs: test
30
+ runs-on: ubuntu-latest
31
+ steps:
32
+ - name: Coveralls Finished
33
+ uses: coverallsapp/github-action@v2
34
+ with:
35
+ github-token: ${{ secrets.GITHUB_TOKEN }}
36
+ parallel-finished: true
37
+ # name: CI
38
+
39
+ # on:
40
+ # push:
41
+ # branches: [ "main" ]
42
+ # pull_request:
43
+ # branches: [ "main" ]
44
+
45
+ # jobs:
46
+ # test:
47
+ # runs-on: ubuntu-latest
48
+ # strategy:
49
+ # fail-fast: false
50
+ # matrix:
51
+ # ruby-version: ['3.2.8', 'jruby-latest']
52
+
53
+ # steps:
54
+ # - uses: actions/checkout@v4
55
+
56
+ # - name: Set up Ruby
57
+ # uses: ruby/setup-ruby@v1
58
+ # with:
59
+ # ruby-version: ${{ matrix.ruby-version }}
60
+ # bundler-cache: true # runs 'bundle install' and caches installed gems
61
+
62
+ # - name: Run tests
63
+ # run: bundle exec rspec
data/.gitignore CHANGED
@@ -13,3 +13,5 @@
13
13
  .rspec_status
14
14
  .byebug_history
15
15
  .aider*
16
+ TAGS
17
+ CONVENTIONS.md
data/.rubocop.yml CHANGED
@@ -26,6 +26,7 @@ Style/FrozenStringLiteralComment:
26
26
  - 'Rakefile'
27
27
  - 'spec/**/*'
28
28
 
29
+ # So that ACH strings in tests won't trigger Cops
29
30
  Layout/LineLength:
30
31
  Max: 108
31
32
 
data/CHANGELOG.md CHANGED
@@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.14] - 2025-07-19
11
+
12
+ - Added markdown and github flavored markdown output
13
+
14
+ - Better architecture for output formatting
15
+
16
+ ## [0.1.13] - 2025-07-16
17
+
18
+ - Fixed an issue with IatEntryDetail where there were multiple
19
+ `reserved` fields. Fixed the associated spec
20
+
21
+ - Fixed column positions for FirstIatAddenda#reserved
22
+
23
+ - Added `child_record_types` for DetailRecordType and AddendaRecordType
24
+ so that parsing work right. Probably need to start using
25
+ `next_record_types` for some of the record types that are in
26
+ `child_record_types` right now
27
+
28
+ - Sorted the fields by position.first when building the matcher and
29
+ the unpack string.
30
+
31
+ - Split the `child_record_types` between the class method and the
32
+ instance method. Concatenated the class method types when the
33
+ instance method is called.
34
+
10
35
  ## [0.1.12] - 2025-07-08
11
36
 
12
37
  - Fixed a bug with parsing files with misshapen lines. Now it
data/README.md CHANGED
@@ -1,11 +1,15 @@
1
1
  # Nacha
2
2
 
3
+ [![Coverage Status](https://coveralls.io/repos/github/badges/shields/badge.svg?branch=master)](https://coveralls.io/github/badges/shields?branch=master)
4
+
3
5
  Validating Ruby ACH parser and generator
4
6
 
5
7
  Format documentation here: http://achrulesonline.org/
6
8
 
7
- The definition of the records exactly mirrors the NACHA documentation so that
8
- development and business can use the same terminology.
9
+ Record Documentation here: https://nachaoperatingrulesonline.org/assets/attachments/25_basic_appendixes.pdf
10
+
11
+ The definition of the records in this gem exactly mirrors the NACHA
12
+ documentation so that development and business can use the same terminology.
9
13
 
10
14
  Work in progress - contributors welcome.
11
15
 
@@ -76,6 +80,10 @@ Parsing starts by looking for a default record type 'Nacha::Record::FileHeader'
76
80
  When that is found, the valid child record types for 'Nacha::Record::FileHeader'
77
81
  are gathered and the subsequent lines are parsed using only those types
78
82
 
83
+ If there is no record type match after exhausing the list of child record types
84
+ for previous records in the hierarchy, the entire list of ACH record types is
85
+ checked
86
+
79
87
  When a record is created, the fields for the instance are created from
80
88
  the field definitions.
81
89
 
data/exe/nacha CHANGED
@@ -2,10 +2,11 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
5
- require 'byebug'
6
5
  require 'thor'
7
6
  require 'json'
8
- require 'nacha' # Assuming this loads the Nacha gem
7
+ require 'nacha'
8
+ require 'nacha/formatter'
9
+ require 'openssl'
9
10
 
10
11
  module Nacha
11
12
  class CLI < Thor
@@ -18,24 +19,24 @@ module Nacha
18
19
  desc "parse FILE", "Parse an ACH file"
19
20
  option :output_file, aliases: "-o"
20
21
  option :format, aliases: "-f", default: "html",
21
- desc: "Output format (html, json, or ach)", enum: %w[html json ach]
22
+ desc: "Output format (html, json, md, or ach)", enum: %w[html json md ach]
23
+ option :md_flavor, default: "common_mark", enum: %w[common_mark github]
22
24
  def parse(file_path)
23
25
  unless File.exist?(file_path)
24
26
  puts "Error: File not found at #{file_path}"
25
27
  exit 1
26
28
  end
27
29
 
28
- ach_file = Nacha.parse(File.open(file_path))
30
+ file = File.open(file_path)
31
+ ach_file = Nacha.parse(file)
29
32
 
30
- # TODO: Determine a user-friendly way to output the parsed data.
31
- # For now, let's print the records.
32
33
  if ach_file && ach_file.is_a?(Array) && !ach_file.empty?
33
34
  if options[:output_file]
34
- File.open(options[:output_file], "w") do |file|
35
- write_output(ach_file, file)
35
+ File.open(options[:output_file], "w") do |f|
36
+ write_output(ach_file, f, file)
36
37
  end
37
38
  else
38
- write_output(ach_file, $stdout)
39
+ write_output(ach_file, $stdout, file)
39
40
  end
40
41
  else
41
42
  puts "Could not parse the file or the file was empty."
@@ -48,52 +49,37 @@ module Nacha
48
49
 
49
50
  private
50
51
 
51
- def write_output(ach_records, io)
52
+ def write_output(ach_records, io, file)
53
+ formatter_options = {
54
+ file_name: File.basename(file.path),
55
+ file_size: file.size,
56
+ number_of_lines: ach_records.size,
57
+ created_at: file.ctime,
58
+ modified_at: file.mtime,
59
+ checksum: OpenSSL::Digest::SHA256.file(File.expand_path(file.path)).hexdigest,
60
+ preamble: HTML_PREAMBLE_FILE,
61
+ postamble: HTML_POSTAMBLE_FILE
62
+ }
63
+
52
64
  case options[:format]
53
- when 'html'
54
- output_html(ach_records, io)
55
- when 'json'
56
- output_json(ach_records, io)
57
65
  when 'ach'
58
66
  output_ach(ach_records, io)
67
+ when 'md'
68
+ formatter_options[:flavor] = options[:md_flavor].to_sym
69
+ formatter = Nacha::Formatter::FormatterFactory.get(:markdown, ach_records, formatter_options)
70
+ io.puts formatter.format
71
+ else
72
+ formatter = Nacha::Formatter::FormatterFactory.get(options[:format].to_sym, ach_records,
73
+ formatter_options)
74
+ io.puts formatter.format
59
75
  end
60
76
  end
61
77
 
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
78
  def output_ach(ach_records, io)
77
79
  ach_records.each do |record|
78
80
  io.puts record.to_ach
79
81
  end
80
82
  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
88
- end
89
-
90
- def html_preamble
91
- @html_preamble ||= File.read(HTML_PREAMBLE_FILE)
92
- end
93
-
94
- def html_postamble
95
- @html_postamble ||= File.read(HTML_POSTAMBLE_FILE)
96
- end
97
83
  end
98
84
  end
99
85
 
@@ -2,21 +2,25 @@
2
2
 
3
3
  require "nacha/has_errors"
4
4
 
5
+ # Validates and formats an ABA routing number.
5
6
  class Nacha::AbaNumber
6
- attr_reader :routing_number, :aba_number
7
+ attr_reader :routing_number
7
8
 
8
9
  include Nacha::HasErrors
9
10
 
10
11
  def initialize(routing_number)
11
12
  @errors = []
12
- self.routing_number = routing_number
13
+ @valid = nil
14
+ @routing_number = routing_number.to_s.strip
13
15
  end
14
16
 
17
+ # :reek:FeatureEnvy
15
18
  def compute_check_digit
16
- n = @routing_number.to_s.strip.ljust(8, '0').chars.collect(&:to_i)
17
- sum = (3 * (n[0] + n[3] + n[6])) +
18
- (7 * (n[1] + n[4] + n[7])) +
19
- (n[2] + n[5])
19
+ digit_array = @routing_number.ljust(8, '0').chars.collect(&:to_i)
20
+ sum = (3 * (digit_array[0] + digit_array[3] + digit_array[6])) +
21
+ (7 * (digit_array[1] + digit_array[4] + digit_array[7])) +
22
+ (digit_array[2] + digit_array[5])
23
+
20
24
  intermediate = (sum % 10)
21
25
  intermediate.zero? ? '0' : (10 - intermediate).to_s
22
26
  end
@@ -32,9 +36,10 @@ class Nacha::AbaNumber
32
36
  end
33
37
 
34
38
  def check_digit
35
- return unless @routing_number.length == 9 && compute_check_digit == @routing_number[8]
39
+ check = @routing_number[8]
40
+ return unless @routing_number.length == 9 && compute_check_digit == check
36
41
 
37
- @routing_number[8]
42
+ check
38
43
  end
39
44
 
40
45
  def valid?
@@ -62,8 +67,9 @@ class Nacha::AbaNumber
62
67
  end
63
68
 
64
69
  def valid_check_digit?
65
- if compute_check_digit != @routing_number[8]
66
- add_error("Incorrect Check Digit \"#{@routing_number[8]}\" should be " \
70
+ check = @routing_number[8]
71
+ if compute_check_digit != check
72
+ add_error("Incorrect Check Digit \"#{check}\" should be " \
67
73
  "\"#{compute_check_digit}\"")
68
74
  false
69
75
  else
@@ -71,11 +77,11 @@ class Nacha::AbaNumber
71
77
  end
72
78
  end
73
79
 
74
- def to_s(with_checkdigit = true)
75
- if with_checkdigit
76
- @routing_number
77
- else
78
- @routing_number[0..7]
79
- end
80
+ def to_s
81
+ @routing_number
82
+ end
83
+
84
+ def to_s_base
85
+ @routing_number[0..7]
80
86
  end
81
87
  end
data/lib/nacha/field.rb CHANGED
@@ -133,37 +133,11 @@ class Nacha::Field
133
133
  # rubocop:enable GitlabSecurity/PublicSend
134
134
  end
135
135
 
136
- def to_json_output
137
- if @json_output
138
- @json_output.reduce(@data) do |memo, operation|
139
- # rubocop:disable GitlabSecurity/PublicSend
140
- memo&.public_send(*operation)
141
- # rubocop:enable GitlabSecurity/PublicSend
142
- end
143
- else
144
- to_s
145
- end
146
- end
147
-
148
136
  def human_name
149
137
  # @human_name ||= @name.to_s.gsub('_', ' ').capitalize
150
138
  @human_name ||= @name.to_s.split('_').map(&:capitalize).join(' ')
151
139
  end
152
140
 
153
- def to_html
154
- tooltip_text = "<span class=\"tooltiptext\" >#{human_name} #{errors.join(' ')}</span>"
155
- field_classes = ["nacha-field tooltip"]
156
- field_classes += ['mandatory'] if mandatory?
157
- field_classes += ['required'] if required?
158
- field_classes += ['optional'] if optional?
159
- field_classes += ['error'] if errors.any?
160
-
161
- ach_string = to_ach.gsub(' ', '&nbsp;')
162
- "<span data-field-name=\"#{@name}\" contentEditable=true " \
163
- "class=\"#{field_classes.join(' ')}\" data-name=\"#{@name}\">" \
164
- "#{ach_string}#{tooltip_text}</span>"
165
- end
166
-
167
141
  def to_s
168
142
  # rubocop:disable GitlabSecurity/PublicSend
169
143
  @data.public_send(*output_conversion).to_s
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+
5
+ module Nacha
6
+ module Formatter
7
+ class Base
8
+ attr_reader :records, :options
9
+
10
+ def initialize(records, options = {})
11
+ @records = records
12
+ @options = options
13
+ end
14
+
15
+ def format
16
+ raise NotImplementedError, 'Subclasses must implement a format method'
17
+ end
18
+
19
+ private
20
+
21
+ def file_statistics
22
+ {
23
+ file_name: options.fetch(:file_name, 'STDIN'),
24
+ file_size: options.fetch(:file_size, 0),
25
+ number_of_lines: options.fetch(:number_of_lines, 0),
26
+ created_at: options.fetch(:created_at, ''),
27
+ modified_at: options.fetch(:modified_at, ''),
28
+ checksum: options.fetch(:checksum, ''),
29
+ number_of_filler_lines: number_of_filler_lines
30
+ }
31
+ end
32
+
33
+ def number_of_filler_lines
34
+ records.count { |r| r.is_a?(Nacha::Record::Filler) }
35
+ end
36
+
37
+ def preamble
38
+ return '' unless options[:preamble]
39
+ return options[:preamble] unless File.exist?(options[:preamble])
40
+
41
+ File.read(options[:preamble])
42
+ end
43
+
44
+ def postamble
45
+ return '' unless options[:postamble]
46
+ return options[:postamble] unless File.exist?(options[:postamble])
47
+
48
+ File.read(options[:postamble])
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'nacha/formatter/base'
4
+
5
+ module Nacha
6
+ module Formatter
7
+ class HtmlFormatter < Base
8
+ def format
9
+ [
10
+ html_preamble,
11
+ file_statistics_html,
12
+ records_html,
13
+ html_postamble
14
+ ].join("\n")
15
+ end
16
+
17
+ private
18
+
19
+ def html_preamble
20
+ preamble_content = options.fetch(:preamble, default_preamble)
21
+ if File.exist?(preamble_content)
22
+ File.read(preamble_content)
23
+ else
24
+ preamble_content
25
+ end
26
+ end
27
+
28
+ def html_postamble
29
+ postamble_content = options.fetch(:postamble, default_postamble)
30
+ if File.exist?(postamble_content)
31
+ File.read(postamble_content)
32
+ else
33
+ postamble_content
34
+ end
35
+ end
36
+
37
+ def file_statistics_html
38
+ stats = file_statistics
39
+ <<~HTML
40
+ <div class="file-statistics">
41
+ <h2>File Information</h2>
42
+ <ul>
43
+ <li><strong>File Name:</strong> #{stats[:file_name]}</li>
44
+ <li><strong>File Size:</strong> #{stats[:file_size]} bytes</li>
45
+ <li><strong>Number of Lines:</strong> #{stats[:number_of_lines]}</li>
46
+ <li><strong>Created At:</strong> #{stats[:created_at]}</li>
47
+ <li><strong>Modified At:</strong> #{stats[:modified_at]}</li>
48
+ <li><strong>Checksum (SHA256):</strong> #{stats[:checksum]}</li>
49
+ <li><strong>Number of Filler Lines:</strong> #{stats[:number_of_filler_lines]}</li>
50
+ </ul>
51
+ </div>
52
+ HTML
53
+ end
54
+
55
+ def records_html
56
+ records.map { |record| record_to_html(record) }.join("\n")
57
+ end
58
+
59
+ def record_to_html(record)
60
+ record_error_class = record.errors.any? ? 'error' : ''
61
+ field_html = record.fields.values.map { |field| field_to_html(field) }.join
62
+
63
+ "<div class=\"nacha-record tooltip #{record.record_type} #{record_error_class}\">" \
64
+ "<span class=\"nacha-record-number\" data-name=\"record-number\">#{Kernel.format('%05d',
65
+ record.line_number)}&nbsp;|&nbsp</span>" \
66
+ "#{field_html}" \
67
+ "<span class=\"record-type\" data-name=\"record-type\">#{record.human_name}</span>" \
68
+ "</div>"
69
+ end
70
+
71
+ def field_to_html(field)
72
+ tooltip_text = "<span class=\"tooltiptext\">#{field.human_name} #{field.errors.join(' ')}</span>"
73
+ field_classes = %w[nacha-field tooltip]
74
+ field_classes << 'mandatory' if field.mandatory?
75
+ field_classes << 'required' if field.required?
76
+ field_classes << 'optional' if field.optional?
77
+ field_classes << 'error' if field.errors.any?
78
+
79
+ ach_string = field.to_ach.gsub(' ', '&nbsp;')
80
+ "<span data-field-name=\"#{field.name}\" contentEditable=true " \
81
+ "class=\"#{field_classes.join(' ')}\" data-name=\"#{field.name}\">" \
82
+ "#{ach_string}#{tooltip_text}</span>"
83
+ end
84
+
85
+ def default_preamble
86
+ stylesheet = options.fetch(:stylesheet, default_stylesheet)
87
+ javascript = options.fetch(:javascript, '')
88
+ <<~HTML
89
+ <!DOCTYPE html>
90
+ <html>
91
+ <head>
92
+ <title>NACHA File</title>
93
+ <style>#{stylesheet}</style>
94
+ <script>#{javascript}</script>
95
+ </head>
96
+ <body>
97
+ HTML
98
+ end
99
+
100
+ def default_postamble
101
+ <<~HTML
102
+ </body>
103
+ </html>
104
+ HTML
105
+ end
106
+
107
+ def default_stylesheet
108
+ <<~CSS
109
+ body { font-family: monospace; }
110
+ .tooltip { position: relative; display: inline-block; border-bottom: 1px dotted black; }
111
+ .tooltip .tooltiptext { visibility: hidden; width: 120px; background-color: black; color: #fff; text-align: center; border-radius: 6px; padding: 5px 0; position: absolute; z-index: 1; }
112
+ .tooltip:hover .tooltiptext { visibility: visible; }
113
+ .nacha-record { white-space: nowrap; }
114
+ .error { background-color: #ffdddd; }
115
+ CSS
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'nacha/formatter/base'
4
+ require 'json'
5
+
6
+ module Nacha
7
+ module Formatter
8
+ class JsonFormatter < Base
9
+ def format
10
+ output = {
11
+ file: file_statistics,
12
+ records: records.map { |record| record_to_h(record) }
13
+ }
14
+
15
+ JSON.pretty_generate(output)
16
+ end
17
+
18
+ private
19
+
20
+ def record_to_h(record)
21
+ {
22
+ nacha_record_type: record.record_type,
23
+ metadata: {
24
+ klass: record.class.name,
25
+ errors: record.errors,
26
+ line_number: record.line_number,
27
+ original_input_line: record.original_input_line
28
+ }
29
+ }.merge(
30
+ record.fields.keys.to_h do |key|
31
+ [key, field_to_json_output(record.fields[key])]
32
+ end
33
+ )
34
+ end
35
+
36
+ def field_to_json_output(field)
37
+ if field.json_output
38
+ # rubocop:disable GitlabSecurity/PublicSend
39
+ field.json_output.reduce(field.raw) do |memo, operation|
40
+ memo&.public_send(*operation)
41
+ end
42
+ # rubocop:enable GitlabSecurity/PublicSend
43
+ else
44
+ field.to_s
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'nacha/formatter/base'
4
+
5
+ module Nacha
6
+ module Formatter
7
+ class MarkdownFormatter < Base
8
+ def format
9
+ [
10
+ file_statistics_markdown,
11
+ records_markdown
12
+ ].join("\n")
13
+ end
14
+
15
+ private
16
+
17
+ def file_statistics_markdown
18
+ stats = file_statistics
19
+ <<~MARKDOWN
20
+ # File Information
21
+
22
+ - **File Name:** #{stats[:file_name]}
23
+ - **File Size:** #{stats[:file_size]} bytes
24
+ - **Number of Lines:** #{stats[:number_of_lines]}
25
+ - **Created At:** #{stats[:created_at]}
26
+ - **Modified At:** #{stats[:modified_at]}
27
+ - **Checksum (SHA256):** #{stats[:checksum]}
28
+ - **Number of Filler Lines:** #{stats[:number_of_filler_lines]}
29
+ MARKDOWN
30
+ end
31
+
32
+ def records_markdown
33
+ records.map { |record| record_to_markdown(record) }.join("\n")
34
+ end
35
+
36
+ def record_to_markdown(record)
37
+ if options[:flavor] == :github
38
+ github_flavored_markdown(record)
39
+ else
40
+ common_mark_markdown(record)
41
+ end
42
+ end
43
+
44
+ def common_mark_markdown(record)
45
+ "## #{record.human_name}\n\n" +
46
+ record.fields.map { |name, field| "* **#{name}:** #{field}" }.join("\n")
47
+ end
48
+
49
+ def github_flavored_markdown(record)
50
+ "### #{record.human_name}\n\n" \
51
+ "| Field | Value |\n" \
52
+ "|-------|-------|\n" +
53
+ record.fields.map { |name, field| "| #{name} | #{field} |" }.join("\n")
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'nacha/formatter/json_formatter'
4
+ require 'nacha/formatter/html_formatter'
5
+ require 'nacha/formatter/markdown_formatter'
6
+
7
+ module Nacha
8
+ module Formatter
9
+ class FormatterFactory
10
+ def self.get(format, records, options = {})
11
+ case format
12
+ when :json
13
+ JsonFormatter.new(records, options)
14
+ when :html
15
+ HtmlFormatter.new(records, options)
16
+ when :markdown
17
+ MarkdownFormatter.new(records, options)
18
+ else
19
+ raise ArgumentError, "Unknown format: #{format}"
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
data/lib/nacha/parser.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'byebug'
4
3
  require 'nacha'
5
4
  require 'nacha/parser_context'
6
5
 
@@ -56,12 +55,19 @@ class Nacha::Parser
56
55
  record = parse_first_by_types(line, record_types)
57
56
  break if record || !parent
58
57
 
59
- record.validate if record
58
+ record_types = valid_record_types(parent.parent)
60
59
  parent = parent.parent
61
- record_types = valid_record_types(parent)
62
60
  end
63
- record.line_number = line_num if record
64
- add_child(parent, record)
61
+ # Check all record types if no record was found
62
+ # TODO: remove this fallback logic
63
+ record ||= parse_first_by_types(line, Nacha.ach_record_types)
64
+
65
+ if record
66
+ record.line_number = line_num
67
+ record.validate
68
+ add_child(parent, record)
69
+ end
70
+
65
71
  record
66
72
  end
67
73
 
@@ -9,7 +9,14 @@ module Nacha
9
9
 
10
10
  module ClassMethods
11
11
  def child_record_types
12
- []
12
+ [
13
+ "Nacha::Record::SecondIatAddenda",
14
+ "Nacha::Record::ThirdIatAddenda",
15
+ "Nacha::Record::FourthIatAddenda",
16
+ "Nacha::Record::FifthIatAddenda",
17
+ "Nacha::Record::SixthIatAddenda",
18
+ "Nacha::Record::SeventhIatAddenda"
19
+ ]
13
20
  end
14
21
 
15
22
  def self.next_record_types
@@ -56,7 +56,9 @@ module Nacha
56
56
  end
57
57
 
58
58
  def unpack_str
59
- @unpack_str ||= definition.values.collect do |field_def|
59
+ @unpack_str ||= definition.values
60
+ .sort { |a, b| a[:position].first <=> b[:position].first }
61
+ .collect do |field_def|
60
62
  Nacha::Field.unpack_str(field_def)
61
63
  end.join.freeze
62
64
  end
@@ -68,7 +70,8 @@ module Nacha
68
70
  @matcher ||= begin
69
71
  output_started = false
70
72
  skipped_output = false
71
- Regexp.new('\A' + definition.values.reverse.collect do |field|
73
+ Regexp.new('\A' + definition.values
74
+ .sort { |a,b| a[:position].first <=> b[:position].first }.reverse.collect do |field|
72
75
  contents = field[:contents]
73
76
  position = field[:position]
74
77
  size = position.size
@@ -83,9 +86,9 @@ module Nacha
83
86
  end
84
87
  elsif contents.match?(/\ANumeric\z/) || contents.match?(/\AYYMMDD\z/)
85
88
  output_started = true
86
- '[0-9 ]' + "{#{size}}"
89
+ "[0-9 ]{#{size}}"
87
90
  elsif output_started
88
- '.' + "{#{size}}"
91
+ ".{#{size}}"
89
92
  else
90
93
  skipped_output = true
91
94
  ''
@@ -181,7 +184,7 @@ module Nacha
181
184
  field.to_html
182
185
  end.join
183
186
  "<div class=\"nacha-record tooltip #{record_type} #{record_error_class}\">" \
184
- "<span class=\"nacha-field\" data-name=\"record-number\">#{format('%05d',
187
+ "<span class=\"nacha-record-number\" data-name=\"record-number\">#{format('%05d',
185
188
  line_number)}&nbsp;|&nbsp</span>" \
186
189
  "#{field_html}" \
187
190
  "<span class=\"record-type\" data-name=\"record-type\">#{human_name}</span>" \
@@ -9,16 +9,17 @@ module Nacha
9
9
 
10
10
  module ClassMethods
11
11
  def child_record_types
12
- []
12
+ [
13
+ 'Nacha::Record::BatchControl'
14
+ ]
13
15
  end
14
16
  end
15
17
 
16
18
  def child_record_types
17
19
  sec = standard_entry_class_code.to_s.capitalize
18
20
  [
19
- "Nacha::Record::#{sec}EntryDetail",
20
- 'Nacha::Record::BatchControl'
21
- ]
21
+ "Nacha::Record::#{sec}EntryDetail"
22
+ ] + self.class.child_record_types
22
23
  end
23
24
  end
24
25
  end
@@ -9,7 +9,10 @@ module Nacha
9
9
 
10
10
  module ClassMethods
11
11
  def child_record_types
12
- []
12
+ [
13
+ "Nacha::Record::FirstIatAddenda"
14
+ # Among other ...
15
+ ]
13
16
  end
14
17
 
15
18
  def next_record_types
@@ -16,7 +16,7 @@ module Nacha
16
16
  nacha_field :foreign_payment_amount, inclusion: 'R', contents: 'Alphameric', position: 7..24
17
17
  nacha_field :foreign_trace_number, inclusion: 'O', contents: 'Alphameric', position: 25..46
18
18
  nacha_field :receiving_company_name, inclusion: 'M', contents: 'Alphameric', position: 47..81
19
- nacha_field :reserved, inclusion: 'M', contents: 'C ', position: 84..87
19
+ nacha_field :reserved, inclusion: 'M', contents: 'C ', position: 82..87
20
20
  nacha_field :entry_detail_sequence_number, inclusion: 'M', contents: 'Numeric', position: 88..94
21
21
  end
22
22
  end
@@ -14,10 +14,10 @@ module Nacha
14
14
  nacha_field :transaction_code, inclusion: 'M', contents: 'Numeric', position: 2..3
15
15
  nacha_field :receiving_dfi_identification, inclusion: 'M', contents: 'TTTTAAAAC', position: 4..12
16
16
  nacha_field :number_of_addenda_records, inclusion: 'M', contents: 'Alphameric', position: 13..16
17
- nacha_field :reserved, inclusion: 'O', contents: 'C ', position: 17..29
17
+ nacha_field :reserved1, inclusion: 'O', contents: 'C ', position: 17..29
18
18
  nacha_field :amount, inclusion: 'M', contents: '$$$$$$$$¢¢', position: 30..39
19
19
  nacha_field :dfi_account_number, inclusion: 'M', contents: 'Numeric', position: 40..74
20
- nacha_field :reserved, inclusion: 'O', contents: 'C ', position: 75..76
20
+ nacha_field :reserved2, inclusion: 'O', contents: 'C ', position: 75..76
21
21
  nacha_field :gateway_operator_ofac_screening_indicator, inclusion: 'O', contents: 'Alphameric',
22
22
  position: 77..77
23
23
  nacha_field :secondary_ofac_screening_indicator, inclusion: 'O', contents: 'Alphameric',
data/lib/nacha/version.rb CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Nacha
4
4
  module Version
5
- STRING = '0.1.12'
5
+ STRING = '0.1.14'
6
6
  end
7
7
  VERSION = Version::STRING
8
8
  end
data/nacha.gemspec CHANGED
@@ -39,4 +39,5 @@ Gem::Specification.new do |spec|
39
39
  spec.add_development_dependency 'rubocop-performance'
40
40
  spec.add_development_dependency 'rubocop-rspec'
41
41
  spec.add_development_dependency 'simplecov'
42
+ spec.add_development_dependency 'simplecov-lcov'
42
43
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nacha
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.12
4
+ version: 0.1.14
5
5
  platform: ruby
6
6
  authors:
7
7
  - David H. Wilkins
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-07-09 00:00:00.000000000 Z
11
+ date: 2025-07-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bigdecimal
@@ -220,6 +220,20 @@ dependencies:
220
220
  - - ">="
221
221
  - !ruby/object:Gem::Version
222
222
  version: '0'
223
+ - !ruby/object:Gem::Dependency
224
+ name: simplecov-lcov
225
+ requirement: !ruby/object:Gem::Requirement
226
+ requirements:
227
+ - - ">="
228
+ - !ruby/object:Gem::Version
229
+ version: '0'
230
+ type: :development
231
+ prerelease: false
232
+ version_requirements: !ruby/object:Gem::Requirement
233
+ requirements:
234
+ - - ">="
235
+ - !ruby/object:Gem::Version
236
+ version: '0'
223
237
  description: Ruby parser for ACH files.
224
238
  email:
225
239
  - dwilkins@conecuh.com
@@ -228,6 +242,7 @@ executables:
228
242
  extensions: []
229
243
  extra_rdoc_files: []
230
244
  files:
245
+ - ".github/workflows/ci.yml"
231
246
  - ".gitignore"
232
247
  - ".gitlab-ci.yml"
233
248
  - ".rspec"
@@ -249,6 +264,11 @@ files:
249
264
  - lib/nacha/aba_number.rb
250
265
  - lib/nacha/ach_date.rb
251
266
  - lib/nacha/field.rb
267
+ - lib/nacha/formatter.rb
268
+ - lib/nacha/formatter/base.rb
269
+ - lib/nacha/formatter/html_formatter.rb
270
+ - lib/nacha/formatter/json_formatter.rb
271
+ - lib/nacha/formatter/markdown_formatter.rb
252
272
  - lib/nacha/has_errors.rb
253
273
  - lib/nacha/numeric.rb
254
274
  - lib/nacha/parser.rb