nacha 0.1.10 → 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 +4 -2
- data/.ruby-version +1 -1
- data/CHANGELOG.md +42 -0
- data/README.md +10 -2
- data/exe/nacha +46 -29
- data/lib/nacha/aba_number.rb +33 -24
- data/lib/nacha/ach_date.rb +15 -8
- data/lib/nacha/field.rb +62 -77
- 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/has_errors.rb +12 -8
- data/lib/nacha/numeric.rb +13 -10
- data/lib/nacha/parser.rb +32 -31
- data/lib/nacha/parser_context.rb +4 -9
- data/lib/nacha/record/ack_entry_detail.rb +15 -8
- data/lib/nacha/record/addenda_record_type.rb +8 -1
- data/lib/nacha/record/adv_batch_control.rb +12 -7
- data/lib/nacha/record/adv_entry_detail.rb +3 -2
- data/lib/nacha/record/adv_file_control.rb +9 -5
- data/lib/nacha/record/adv_file_header.rb +11 -6
- data/lib/nacha/record/arc_entry_detail.rb +2 -2
- data/lib/nacha/record/base.rb +124 -106
- data/lib/nacha/record/batch_control.rb +13 -7
- data/lib/nacha/record/batch_header.rb +20 -11
- data/lib/nacha/record/batch_header_record_type.rb +5 -4
- data/lib/nacha/record/boc_entry_detail.rb +3 -2
- data/lib/nacha/record/ccd_addenda.rb +2 -2
- data/lib/nacha/record/ccd_entry_detail.rb +3 -2
- data/lib/nacha/record/cie_addenda.rb +2 -2
- data/lib/nacha/record/cie_entry_detail.rb +5 -3
- data/lib/nacha/record/ctx_addenda.rb +2 -2
- data/lib/nacha/record/ctx_corporate_entry_detail.rb +2 -2
- data/lib/nacha/record/detail_record_type.rb +4 -1
- data/lib/nacha/record/dne_addenda.rb +2 -2
- data/lib/nacha/record/dne_entry_detail.rb +6 -4
- data/lib/nacha/record/enr_addenda.rb +3 -2
- data/lib/nacha/record/enr_entry_detail.rb +4 -3
- data/lib/nacha/record/fifth_iat_addenda.rb +8 -4
- data/lib/nacha/record/file_control.rb +9 -5
- data/lib/nacha/record/file_control_record_type.rb +1 -1
- data/lib/nacha/record/file_header.rb +12 -8
- data/lib/nacha/record/file_header_record_type.rb +1 -1
- data/lib/nacha/record/filler.rb +3 -3
- data/lib/nacha/record/filler_record_type.rb +3 -1
- data/lib/nacha/record/first_iat_addenda.rb +4 -3
- data/lib/nacha/record/fourth_iat_addenda.rb +7 -4
- data/lib/nacha/record/iat_batch_header.rb +5 -3
- data/lib/nacha/record/iat_entry_detail.rb +9 -6
- data/lib/nacha/record/iat_foreign_coorespondent_bank_information_addenda.rb +10 -6
- data/lib/nacha/record/iat_remittance_information_addenda.rb +3 -2
- data/lib/nacha/record/mte_addenda.rb +4 -3
- data/lib/nacha/record/mte_entry_detail.rb +5 -3
- data/lib/nacha/record/pop_entry_detail.rb +3 -2
- data/lib/nacha/record/pos_addenda.rb +6 -3
- data/lib/nacha/record/pos_entry_detail.rb +5 -3
- data/lib/nacha/record/ppd_addenda.rb +3 -2
- data/lib/nacha/record/ppd_entry_detail.rb +5 -3
- data/lib/nacha/record/rck_entry_detail.rb +3 -2
- data/lib/nacha/record/second_iat_addenda.rb +3 -2
- data/lib/nacha/record/seventh_iat_addenda.rb +3 -2
- data/lib/nacha/record/shr_addenda.rb +5 -3
- data/lib/nacha/record/shr_entry_detail.rb +3 -2
- data/lib/nacha/record/sixth_iat_addenda.rb +5 -3
- data/lib/nacha/record/tel_entry_detail.rb +5 -3
- data/lib/nacha/record/third_iat_addenda.rb +3 -2
- data/lib/nacha/record/trc_entry_detail.rb +3 -2
- data/lib/nacha/record/trx_addenda.rb +3 -2
- data/lib/nacha/record/trx_entry_detail.rb +5 -3
- data/lib/nacha/record/validations/field_validations.rb +26 -14
- data/lib/nacha/record/validations/record_validations.rb +2 -1
- data/lib/nacha/record/web_addenda.rb +3 -2
- data/lib/nacha/record/web_entry_detail.rb +5 -3
- data/lib/nacha/record/xck_entry_detail.rb +3 -2
- data/lib/nacha/version.rb +4 -1
- data/lib/nacha.rb +21 -14
- data/nacha.gemspec +14 -16
- metadata +42 -8
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
@@ -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,9 @@ Style/FrozenStringLiteralComment:
|
|
25
26
|
- 'Rakefile'
|
26
27
|
- 'spec/**/*'
|
27
28
|
|
28
|
-
|
29
|
-
|
29
|
+
# So that ACH strings in tests won't trigger Cops
|
30
|
+
Layout/LineLength:
|
31
|
+
Max: 108
|
30
32
|
|
31
33
|
Naming/FileName:
|
32
34
|
ExpectMatchingDefinition: true
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
3.
|
1
|
+
3.2.8
|
data/CHANGELOG.md
CHANGED
@@ -7,6 +7,48 @@ 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
|
+
|
35
|
+
## [0.1.12] - 2025-07-08
|
36
|
+
|
37
|
+
- Fixed a bug with parsing files with misshapen lines. Now it
|
38
|
+
_should_ handle lines that are shorter than 94 characters _and_ have
|
39
|
+
(CR|CRLF|LF) as the terminating character.
|
40
|
+
|
41
|
+
|
42
|
+
|
43
|
+
## [0.1.11] - 2025-07-05
|
44
|
+
|
45
|
+
### Output formatting options
|
46
|
+
- use -f [html|json|ach] or --format [html|json|ach] to change
|
47
|
+
|
48
|
+
- use -o filename or --output filename to specify a file to output
|
49
|
+
|
50
|
+
- Aider and I fixed a bunch of rubocop offenses and refactored some code
|
51
|
+
|
10
52
|
## [0.1.10] - 2025-07-01
|
11
53
|
|
12
54
|
- Added ability to get a list of possible record types and
|
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
@@ -1,29 +1,43 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# frozen_string_literal: true
|
3
|
+
|
4
|
+
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
3
5
|
require 'thor'
|
4
|
-
require '
|
6
|
+
require 'json'
|
7
|
+
require 'nacha'
|
8
|
+
require 'nacha/formatter'
|
9
|
+
require 'openssl'
|
5
10
|
|
6
11
|
module Nacha
|
7
12
|
class CLI < Thor
|
8
13
|
TEMPLATES_DIR = File.join(Gem::Specification.find_by_name("nacha").gem_dir,
|
9
|
-
|
14
|
+
"templates").freeze
|
10
15
|
|
11
16
|
HTML_PREAMBLE_FILE = File.join(TEMPLATES_DIR, "html_preamble.html")
|
12
17
|
HTML_POSTAMBLE_FILE = File.join(TEMPLATES_DIR, "html_postamble.html")
|
13
18
|
|
14
19
|
desc "parse FILE", "Parse an ACH file"
|
20
|
+
option :output_file, aliases: "-o"
|
21
|
+
option :format, aliases: "-f", default: "html",
|
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]
|
15
24
|
def parse(file_path)
|
16
25
|
unless File.exist?(file_path)
|
17
26
|
puts "Error: File not found at #{file_path}"
|
18
27
|
exit 1
|
19
28
|
end
|
20
29
|
|
21
|
-
|
30
|
+
file = File.open(file_path)
|
31
|
+
ach_file = Nacha.parse(file)
|
22
32
|
|
23
|
-
# TODO: Determine a user-friendly way to output the parsed data.
|
24
|
-
# For now, let's print the records.
|
25
33
|
if ach_file && ach_file.is_a?(Array) && !ach_file.empty?
|
26
|
-
|
34
|
+
if options[:output_file]
|
35
|
+
File.open(options[:output_file], "w") do |f|
|
36
|
+
write_output(ach_file, f, file)
|
37
|
+
end
|
38
|
+
else
|
39
|
+
write_output(ach_file, $stdout, file)
|
40
|
+
end
|
27
41
|
else
|
28
42
|
puts "Could not parse the file or the file was empty."
|
29
43
|
end
|
@@ -35,32 +49,35 @@ module Nacha
|
|
35
49
|
|
36
50
|
private
|
37
51
|
|
38
|
-
def
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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
|
+
}
|
46
63
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
64
|
+
case options[:format]
|
65
|
+
when 'ach'
|
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
|
75
|
+
end
|
53
76
|
end
|
54
77
|
|
55
|
-
def
|
56
|
-
|
57
|
-
|
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
|
78
|
+
def output_ach(ach_records, io)
|
79
|
+
ach_records.each do |record|
|
80
|
+
io.puts record.to_ach
|
64
81
|
end
|
65
82
|
end
|
66
83
|
end
|
data/lib/nacha/aba_number.rb
CHANGED
@@ -2,22 +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
7
|
attr_reader :routing_number
|
7
|
-
attr_reader :aba_number
|
8
8
|
|
9
|
-
include HasErrors
|
9
|
+
include Nacha::HasErrors
|
10
10
|
|
11
11
|
def initialize(routing_number)
|
12
12
|
@errors = []
|
13
|
-
|
13
|
+
@valid = nil
|
14
|
+
@routing_number = routing_number.to_s.strip
|
14
15
|
end
|
15
16
|
|
17
|
+
# :reek:FeatureEnvy
|
16
18
|
def compute_check_digit
|
17
|
-
|
18
|
-
sum = (3 * (
|
19
|
-
(7 * (
|
20
|
-
(
|
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
|
+
|
21
24
|
intermediate = (sum % 10)
|
22
25
|
intermediate.zero? ? '0' : (10 - intermediate).to_s
|
23
26
|
end
|
@@ -33,19 +36,22 @@ class Nacha::AbaNumber
|
|
33
36
|
end
|
34
37
|
|
35
38
|
def check_digit
|
36
|
-
|
39
|
+
check = @routing_number[8]
|
40
|
+
return unless @routing_number.length == 9 && compute_check_digit == check
|
41
|
+
|
42
|
+
check
|
37
43
|
end
|
38
44
|
|
39
45
|
def valid?
|
40
46
|
@valid ||= if valid_routing_number_length?
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
47
|
+
if @routing_number.length == 9
|
48
|
+
valid_check_digit?
|
49
|
+
else # 8 digits is valid
|
50
|
+
true
|
51
|
+
end
|
52
|
+
else
|
53
|
+
false
|
45
54
|
end
|
46
|
-
else
|
47
|
-
false
|
48
|
-
end
|
49
55
|
end
|
50
56
|
|
51
57
|
def valid_routing_number_length?
|
@@ -54,25 +60,28 @@ class Nacha::AbaNumber
|
|
54
60
|
if [9, 10].include?(actual_length)
|
55
61
|
true
|
56
62
|
else
|
57
|
-
add_error("Routing number must be 8 or 9 digits long, but was
|
63
|
+
add_error("Routing number must be 8 or 9 digits long, but was " \
|
64
|
+
"#{actual_length} digits long.")
|
58
65
|
false
|
59
66
|
end
|
60
67
|
end
|
61
68
|
|
62
69
|
def valid_check_digit?
|
63
|
-
|
64
|
-
|
70
|
+
check = @routing_number[8]
|
71
|
+
if compute_check_digit != check
|
72
|
+
add_error("Incorrect Check Digit \"#{check}\" should be " \
|
73
|
+
"\"#{compute_check_digit}\"")
|
65
74
|
false
|
66
75
|
else
|
67
76
|
true
|
68
77
|
end
|
69
78
|
end
|
70
79
|
|
71
|
-
def to_s
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
80
|
+
def to_s
|
81
|
+
@routing_number
|
82
|
+
end
|
83
|
+
|
84
|
+
def to_s_base
|
85
|
+
@routing_number[0..7]
|
77
86
|
end
|
78
87
|
end
|
data/lib/nacha/ach_date.rb
CHANGED
@@ -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
|
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
|
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
|
-
|
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
|
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
|
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}.
|
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
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
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
|
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
|
-
|
108
|
-
|
109
|
-
|
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,30 +123,14 @@ 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
|
-
|
164
|
-
|
165
|
-
|
166
|
-
def to_json_output
|
167
|
-
if @json_output
|
168
|
-
@json_output.reduce(@data) do |output, operation|
|
169
|
-
output = output.send(*operation) if output
|
170
|
-
end
|
171
|
-
else
|
172
|
-
to_s
|
173
|
-
end
|
131
|
+
# rubocop:disable GitlabSecurity/PublicSend
|
132
|
+
str.public_send(justification, position.count, fill_char)
|
133
|
+
# rubocop:enable GitlabSecurity/PublicSend
|
174
134
|
end
|
175
135
|
|
176
136
|
def human_name
|
@@ -178,25 +138,50 @@ class Nacha::Field
|
|
178
138
|
@human_name ||= @name.to_s.split('_').map(&:capitalize).join(' ')
|
179
139
|
end
|
180
140
|
|
181
|
-
def to_html
|
182
|
-
tooltip_text = "<span class=\"tooltiptext\" >#{human_name} #{errors.join(' ')}</span>"
|
183
|
-
field_classes = ["nacha-field tooltip"]
|
184
|
-
field_classes += ['mandatory'] if mandatory?
|
185
|
-
field_classes += ['required'] if required?
|
186
|
-
field_classes += ['optional'] if optional?
|
187
|
-
field_classes += ['error'] if errors.any?
|
188
|
-
|
189
|
-
ach_string = to_ach.gsub(' ', ' ')
|
190
|
-
"<span data-field-name=\"#{@name}\" contentEditable=true class=\"#{field_classes.join(' ')}\" data-name=\"#{@name}\">#{ach_string}" +
|
191
|
-
tooltip_text.to_s +
|
192
|
-
"</span>"
|
193
|
-
end
|
194
|
-
|
195
141
|
def to_s
|
196
|
-
|
142
|
+
# rubocop:disable GitlabSecurity/PublicSend
|
143
|
+
@data.public_send(*output_conversion).to_s
|
144
|
+
# rubocop:enable GitlabSecurity/PublicSend
|
197
145
|
end
|
198
146
|
|
199
147
|
def raw
|
200
148
|
@data
|
201
149
|
end
|
150
|
+
|
151
|
+
private
|
152
|
+
|
153
|
+
def validate_definition_attributes
|
154
|
+
add_error("'inclusion' must be present for a field definition.") unless @inclusion
|
155
|
+
add_error("'position' must be present for a field definition.") unless @position
|
156
|
+
add_error("'contents' must be present for a field definition.") unless @contents
|
157
|
+
end
|
158
|
+
|
159
|
+
def validate_data_presence
|
160
|
+
return unless @data_assigned && (mandatory? || required?)
|
161
|
+
return unless @input_data.nil? || @input_data.to_s.empty?
|
162
|
+
return if @contents.match?(/\AC( *)\z/)
|
163
|
+
|
164
|
+
add_error("'#{human_name}' is a required field and cannot be blank.")
|
165
|
+
end
|
166
|
+
|
167
|
+
def validate_numeric_format
|
168
|
+
return unless @data_type == Nacha::Numeric && @input_data.to_s.strip.match(/\D/)
|
169
|
+
|
170
|
+
add_error("Invalid characters in numeric field '#{human_name}'. " \
|
171
|
+
"Got '#{@input_data}'.")
|
172
|
+
end
|
173
|
+
|
174
|
+
def run_custom_validator
|
175
|
+
return unless @validator && @data.is_a?(@data_type)
|
176
|
+
|
177
|
+
# rubocop:disable GitlabSecurity/PublicSend
|
178
|
+
is_valid = @data.public_send(@validator)
|
179
|
+
# rubocop:enable GitlabSecurity/PublicSend
|
180
|
+
|
181
|
+
@data.errors.each { |e| add_error(e) } if @data.respond_to?(:errors) && @data.errors&.any?
|
182
|
+
|
183
|
+
return if is_valid || errors.any?
|
184
|
+
|
185
|
+
add_error("'#{human_name}' is invalid. Got '#{@input_data}'.")
|
186
|
+
end
|
202
187
|
end
|
@@ -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
|