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 +4 -4
- data/.github/workflows/ci.yml +63 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +1 -0
- data/CHANGELOG.md +25 -0
- data/README.md +10 -2
- data/exe/nacha +30 -44
- data/lib/nacha/aba_number.rb +22 -16
- data/lib/nacha/field.rb +0 -26
- data/lib/nacha/formatter/base.rb +52 -0
- data/lib/nacha/formatter/html_formatter.rb +119 -0
- data/lib/nacha/formatter/json_formatter.rb +49 -0
- data/lib/nacha/formatter/markdown_formatter.rb +57 -0
- data/lib/nacha/formatter.rb +24 -0
- data/lib/nacha/parser.rb +11 -5
- data/lib/nacha/record/addenda_record_type.rb +8 -1
- data/lib/nacha/record/base.rb +8 -5
- data/lib/nacha/record/batch_header_record_type.rb +5 -4
- data/lib/nacha/record/detail_record_type.rb +4 -1
- data/lib/nacha/record/first_iat_addenda.rb +1 -1
- data/lib/nacha/record/iat_entry_detail.rb +2 -2
- data/lib/nacha/version.rb +1 -1
- data/nacha.gemspec +1 -0
- metadata +22 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 986181f9dfa47368890ac283705b188bfef4733adff641590cc889f491cf5ed2
|
4
|
+
data.tar.gz: 1aff3dd6aedd49cf8f1e9d2562131b28832452fd0332a71429f49955aa19e858
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
data/.rubocop.yml
CHANGED
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
|
+
[](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
|
-
|
8
|
-
|
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'
|
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
|
-
|
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 |
|
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
|
|
data/lib/nacha/aba_number.rb
CHANGED
@@ -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
|
7
|
+
attr_reader :routing_number
|
7
8
|
|
8
9
|
include Nacha::HasErrors
|
9
10
|
|
10
11
|
def initialize(routing_number)
|
11
12
|
@errors = []
|
12
|
-
|
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
|
-
|
17
|
-
sum = (3 * (
|
18
|
-
(7 * (
|
19
|
-
(
|
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
|
-
|
39
|
+
check = @routing_number[8]
|
40
|
+
return unless @routing_number.length == 9 && compute_check_digit == check
|
36
41
|
|
37
|
-
|
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
|
-
|
66
|
-
|
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
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
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(' ', ' ')
|
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)} | </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(' ', ' ')
|
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
|
-
|
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
|
64
|
-
|
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
|
data/lib/nacha/record/base.rb
CHANGED
@@ -56,7 +56,9 @@ module Nacha
|
|
56
56
|
end
|
57
57
|
|
58
58
|
def unpack_str
|
59
|
-
@unpack_str ||= definition.values
|
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
|
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
|
-
|
89
|
+
"[0-9 ]{#{size}}"
|
87
90
|
elsif output_started
|
88
|
-
|
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-
|
187
|
+
"<span class=\"nacha-record-number\" data-name=\"record-number\">#{format('%05d',
|
185
188
|
line_number)} | </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
|
-
|
21
|
-
]
|
21
|
+
"Nacha::Record::#{sec}EntryDetail"
|
22
|
+
] + self.class.child_record_types
|
22
23
|
end
|
23
24
|
end
|
24
25
|
end
|
@@ -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:
|
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 :
|
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 :
|
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
data/nacha.gemspec
CHANGED
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.
|
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-
|
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
|