nacha 0.1.10 → 0.1.12
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/.rubocop.yml +3 -2
- data/.ruby-version +1 -1
- data/CHANGELOG.md +17 -0
- data/exe/nacha +51 -20
- data/lib/nacha/aba_number.rb +17 -14
- data/lib/nacha/ach_date.rb +15 -8
- data/lib/nacha/field.rb +69 -58
- data/lib/nacha/has_errors.rb +12 -8
- data/lib/nacha/numeric.rb +13 -10
- data/lib/nacha/parser.rb +22 -27
- data/lib/nacha/parser_context.rb +4 -9
- data/lib/nacha/record/ack_entry_detail.rb +15 -8
- 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 +121 -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 +1 -1
- 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/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 +3 -2
- 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 +7 -4
- 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 +13 -16
- metadata +22 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 737ec495f2219167b7ef5ec5a959a6b205a4cf551eba2fdee6b5d3f0324b0735
|
4
|
+
data.tar.gz: 67f479ed3d4f1affb7d7a0f3fb92168bc2554a84b33cca2c3108d884a36e3989
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3fbae7e0193e68fa8fda30e32ff8272c4380556352fd6b64d3fdd6eb806e974ac147952a033603810f712f06b5437207a7a0d6a7d84ae4abe435c87314716329
|
7
|
+
data.tar.gz: 666b409414698a8502a69d02dca7cdc35f929cda66c66a6ef68430701d252f4640ed8aaaeeec1ba0064c5ff49a94befe189a69757fb5903142eede3a706dcfbe
|
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,8 @@ Style/FrozenStringLiteralComment:
|
|
25
26
|
- 'Rakefile'
|
26
27
|
- 'spec/**/*'
|
27
28
|
|
28
|
-
|
29
|
-
Max:
|
29
|
+
Layout/LineLength:
|
30
|
+
Max: 108
|
30
31
|
|
31
32
|
Naming/FileName:
|
32
33
|
ExpectMatchingDefinition: true
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
3.
|
1
|
+
3.2.8
|
data/CHANGELOG.md
CHANGED
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
7
7
|
|
8
8
|
## [Unreleased]
|
9
9
|
|
10
|
+
## [0.1.12] - 2025-07-08
|
11
|
+
|
12
|
+
- Fixed a bug with parsing files with misshapen lines. Now it
|
13
|
+
_should_ handle lines that are shorter than 94 characters _and_ have
|
14
|
+
(CR|CRLF|LF) as the terminating character.
|
15
|
+
|
16
|
+
|
17
|
+
|
18
|
+
## [0.1.11] - 2025-07-05
|
19
|
+
|
20
|
+
### Output formatting options
|
21
|
+
- use -f [html|json|ach] or --format [html|json|ach] to change
|
22
|
+
|
23
|
+
- use -o filename or --output filename to specify a file to output
|
24
|
+
|
25
|
+
- Aider and I fixed a bunch of rubocop offenses and refactored some code
|
26
|
+
|
10
27
|
## [0.1.10] - 2025-07-01
|
11
28
|
|
12
29
|
- Added ability to get a list of possible record types and
|
data/exe/nacha
CHANGED
@@ -1,29 +1,42 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# frozen_string_literal: true
|
3
|
+
|
4
|
+
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
5
|
+
require 'byebug'
|
3
6
|
require 'thor'
|
7
|
+
require 'json'
|
4
8
|
require 'nacha' # Assuming this loads the Nacha gem
|
5
9
|
|
6
10
|
module Nacha
|
7
11
|
class CLI < Thor
|
8
12
|
TEMPLATES_DIR = File.join(Gem::Specification.find_by_name("nacha").gem_dir,
|
9
|
-
|
13
|
+
"templates").freeze
|
10
14
|
|
11
15
|
HTML_PREAMBLE_FILE = File.join(TEMPLATES_DIR, "html_preamble.html")
|
12
16
|
HTML_POSTAMBLE_FILE = File.join(TEMPLATES_DIR, "html_postamble.html")
|
13
17
|
|
14
18
|
desc "parse FILE", "Parse an ACH file"
|
19
|
+
option :output_file, aliases: "-o"
|
20
|
+
option :format, aliases: "-f", default: "html",
|
21
|
+
desc: "Output format (html, json, or ach)", enum: %w[html json ach]
|
15
22
|
def parse(file_path)
|
16
23
|
unless File.exist?(file_path)
|
17
24
|
puts "Error: File not found at #{file_path}"
|
18
25
|
exit 1
|
19
26
|
end
|
20
27
|
|
21
|
-
ach_file =
|
28
|
+
ach_file = Nacha.parse(File.open(file_path))
|
22
29
|
|
23
30
|
# TODO: Determine a user-friendly way to output the parsed data.
|
24
31
|
# For now, let's print the records.
|
25
32
|
if ach_file && ach_file.is_a?(Array) && !ach_file.empty?
|
26
|
-
|
33
|
+
if options[:output_file]
|
34
|
+
File.open(options[:output_file], "w") do |file|
|
35
|
+
write_output(ach_file, file)
|
36
|
+
end
|
37
|
+
else
|
38
|
+
write_output(ach_file, $stdout)
|
39
|
+
end
|
27
40
|
else
|
28
41
|
puts "Could not parse the file or the file was empty."
|
29
42
|
end
|
@@ -35,13 +48,43 @@ module Nacha
|
|
35
48
|
|
36
49
|
private
|
37
50
|
|
38
|
-
def
|
39
|
-
|
40
|
-
|
51
|
+
def write_output(ach_records, io)
|
52
|
+
case options[:format]
|
53
|
+
when 'html'
|
54
|
+
output_html(ach_records, io)
|
55
|
+
when 'json'
|
56
|
+
output_json(ach_records, io)
|
57
|
+
when 'ach'
|
58
|
+
output_ach(ach_records, io)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
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
|
+
def output_ach(ach_records, io)
|
41
77
|
ach_records.each do |record|
|
42
|
-
|
78
|
+
io.puts record.to_ach
|
43
79
|
end
|
44
|
-
|
80
|
+
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
|
45
88
|
end
|
46
89
|
|
47
90
|
def html_preamble
|
@@ -51,18 +94,6 @@ module Nacha
|
|
51
94
|
def html_postamble
|
52
95
|
@html_postamble ||= File.read(HTML_POSTAMBLE_FILE)
|
53
96
|
end
|
54
|
-
|
55
|
-
def display_child(level, record)
|
56
|
-
# Attempt to call a summary or to_s method if it exists,
|
57
|
-
# otherwise inspect the record.
|
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
|
64
|
-
end
|
65
|
-
end
|
66
97
|
end
|
67
98
|
end
|
68
99
|
|
data/lib/nacha/aba_number.rb
CHANGED
@@ -3,10 +3,9 @@
|
|
3
3
|
require "nacha/has_errors"
|
4
4
|
|
5
5
|
class Nacha::AbaNumber
|
6
|
-
attr_reader :routing_number
|
7
|
-
attr_reader :aba_number
|
6
|
+
attr_reader :routing_number, :aba_number
|
8
7
|
|
9
|
-
include HasErrors
|
8
|
+
include Nacha::HasErrors
|
10
9
|
|
11
10
|
def initialize(routing_number)
|
12
11
|
@errors = []
|
@@ -33,19 +32,21 @@ class Nacha::AbaNumber
|
|
33
32
|
end
|
34
33
|
|
35
34
|
def check_digit
|
36
|
-
|
35
|
+
return unless @routing_number.length == 9 && compute_check_digit == @routing_number[8]
|
36
|
+
|
37
|
+
@routing_number[8]
|
37
38
|
end
|
38
39
|
|
39
40
|
def valid?
|
40
41
|
@valid ||= if valid_routing_number_length?
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
42
|
+
if @routing_number.length == 9
|
43
|
+
valid_check_digit?
|
44
|
+
else # 8 digits is valid
|
45
|
+
true
|
46
|
+
end
|
47
|
+
else
|
48
|
+
false
|
45
49
|
end
|
46
|
-
else
|
47
|
-
false
|
48
|
-
end
|
49
50
|
end
|
50
51
|
|
51
52
|
def valid_routing_number_length?
|
@@ -54,14 +55,16 @@ class Nacha::AbaNumber
|
|
54
55
|
if [9, 10].include?(actual_length)
|
55
56
|
true
|
56
57
|
else
|
57
|
-
add_error("Routing number must be 8 or 9 digits long, but was
|
58
|
+
add_error("Routing number must be 8 or 9 digits long, but was " \
|
59
|
+
"#{actual_length} digits long.")
|
58
60
|
false
|
59
61
|
end
|
60
62
|
end
|
61
63
|
|
62
64
|
def valid_check_digit?
|
63
|
-
if compute_check_digit != @routing_number
|
64
|
-
add_error("Incorrect Check Digit \"#{@routing_number
|
65
|
+
if compute_check_digit != @routing_number[8]
|
66
|
+
add_error("Incorrect Check Digit \"#{@routing_number[8]}\" should be " \
|
67
|
+
"\"#{compute_check_digit}\"")
|
65
68
|
false
|
66
69
|
else
|
67
70
|
true
|
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,26 +123,22 @@ 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
|
-
|
131
|
+
# rubocop:disable GitlabSecurity/PublicSend
|
132
|
+
str.public_send(justification, position.count, fill_char)
|
133
|
+
# rubocop:enable GitlabSecurity/PublicSend
|
164
134
|
end
|
165
135
|
|
166
136
|
def to_json_output
|
167
137
|
if @json_output
|
168
|
-
@json_output.reduce(@data) do |
|
169
|
-
|
138
|
+
@json_output.reduce(@data) do |memo, operation|
|
139
|
+
# rubocop:disable GitlabSecurity/PublicSend
|
140
|
+
memo&.public_send(*operation)
|
141
|
+
# rubocop:enable GitlabSecurity/PublicSend
|
170
142
|
end
|
171
143
|
else
|
172
144
|
to_s
|
@@ -187,16 +159,55 @@ class Nacha::Field
|
|
187
159
|
field_classes += ['error'] if errors.any?
|
188
160
|
|
189
161
|
ach_string = to_ach.gsub(' ', ' ')
|
190
|
-
"<span data-field-name=\"#{@name}\" contentEditable=true
|
191
|
-
|
192
|
-
"</span>"
|
162
|
+
"<span data-field-name=\"#{@name}\" contentEditable=true " \
|
163
|
+
"class=\"#{field_classes.join(' ')}\" data-name=\"#{@name}\">" \
|
164
|
+
"#{ach_string}#{tooltip_text}</span>"
|
193
165
|
end
|
194
166
|
|
195
167
|
def to_s
|
196
|
-
|
168
|
+
# rubocop:disable GitlabSecurity/PublicSend
|
169
|
+
@data.public_send(*output_conversion).to_s
|
170
|
+
# rubocop:enable GitlabSecurity/PublicSend
|
197
171
|
end
|
198
172
|
|
199
173
|
def raw
|
200
174
|
@data
|
201
175
|
end
|
176
|
+
|
177
|
+
private
|
178
|
+
|
179
|
+
def validate_definition_attributes
|
180
|
+
add_error("'inclusion' must be present for a field definition.") unless @inclusion
|
181
|
+
add_error("'position' must be present for a field definition.") unless @position
|
182
|
+
add_error("'contents' must be present for a field definition.") unless @contents
|
183
|
+
end
|
184
|
+
|
185
|
+
def validate_data_presence
|
186
|
+
return unless @data_assigned && (mandatory? || required?)
|
187
|
+
return unless @input_data.nil? || @input_data.to_s.empty?
|
188
|
+
return if @contents.match?(/\AC( *)\z/)
|
189
|
+
|
190
|
+
add_error("'#{human_name}' is a required field and cannot be blank.")
|
191
|
+
end
|
192
|
+
|
193
|
+
def validate_numeric_format
|
194
|
+
return unless @data_type == Nacha::Numeric && @input_data.to_s.strip.match(/\D/)
|
195
|
+
|
196
|
+
add_error("Invalid characters in numeric field '#{human_name}'. " \
|
197
|
+
"Got '#{@input_data}'.")
|
198
|
+
end
|
199
|
+
|
200
|
+
def run_custom_validator
|
201
|
+
return unless @validator && @data.is_a?(@data_type)
|
202
|
+
|
203
|
+
# rubocop:disable GitlabSecurity/PublicSend
|
204
|
+
is_valid = @data.public_send(@validator)
|
205
|
+
# rubocop:enable GitlabSecurity/PublicSend
|
206
|
+
|
207
|
+
@data.errors.each { |e| add_error(e) } if @data.respond_to?(:errors) && @data.errors&.any?
|
208
|
+
|
209
|
+
return if is_valid || errors.any?
|
210
|
+
|
211
|
+
add_error("'#{human_name}' is invalid. Got '#{@input_data}'.")
|
212
|
+
end
|
202
213
|
end
|
data/lib/nacha/has_errors.rb
CHANGED
@@ -1,12 +1,16 @@
|
|
1
|
-
|
2
|
-
attr_reader :errors
|
1
|
+
# frozen_string_literal: true
|
3
2
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
3
|
+
module Nacha
|
4
|
+
module HasErrors
|
5
|
+
attr_reader :errors
|
6
|
+
|
7
|
+
def add_error(message)
|
8
|
+
@errors ||= []
|
9
|
+
@errors << message
|
10
|
+
end
|
8
11
|
|
9
|
-
|
10
|
-
|
12
|
+
def has_errors?
|
13
|
+
!@errors.nil? && !@errors.empty?
|
14
|
+
end
|
11
15
|
end
|
12
16
|
end
|
data/lib/nacha/numeric.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
class Nacha::Numeric
|
3
|
-
def initialize
|
4
|
+
def initialize(val = nil)
|
4
5
|
self.value = val
|
5
6
|
end
|
6
7
|
|
@@ -17,14 +18,14 @@ class Nacha::Numeric
|
|
17
18
|
def value=(val)
|
18
19
|
if val.is_a?(String)
|
19
20
|
@value = val.dup
|
20
|
-
if
|
21
|
+
if !val.strip.empty?
|
21
22
|
@op_value = BigDecimal(val.strip)
|
22
23
|
@value = val.dup
|
23
24
|
else
|
24
|
-
@op_value = BigDecimal(0)
|
25
|
+
@op_value = BigDecimal('0')
|
25
26
|
end
|
26
27
|
elsif val.nil?
|
27
|
-
@value = BigDecimal(0)
|
28
|
+
@value = BigDecimal('0')
|
28
29
|
@op_value = @value
|
29
30
|
else
|
30
31
|
@value = BigDecimal(val)
|
@@ -36,8 +37,8 @@ class Nacha::Numeric
|
|
36
37
|
@value ? @value.to_i.to_s : nil
|
37
38
|
end
|
38
39
|
|
39
|
-
def respond_to_missing?(method_name,
|
40
|
-
@op_value.respond_to?
|
40
|
+
def respond_to_missing?(method_name, _include_private = false)
|
41
|
+
@op_value.respond_to?(method_name)
|
41
42
|
end
|
42
43
|
|
43
44
|
# @op_value is the value for operations. @value may still be a string
|
@@ -45,20 +46,22 @@ class Nacha::Numeric
|
|
45
46
|
# should be checked to see if the operation is valid for it, not
|
46
47
|
# necessarily the potentially string @value
|
47
48
|
def method_missing(method_name, *args, &block)
|
48
|
-
if @op_value.respond_to?
|
49
|
+
if @op_value.respond_to?(method_name)
|
49
50
|
old_op_value = @op_value.dup
|
50
|
-
|
51
|
-
|
51
|
+
# rubocop:disable GitlabSecurity/PublicSend
|
52
|
+
if method_name.to_s.end_with?('!')
|
52
53
|
@op_value.send(method_name, *args, &block)
|
53
54
|
return_value = @op_value
|
54
55
|
else
|
55
56
|
return_value = @op_value.send(method_name, *args, &block)
|
56
|
-
# rubocop:enable GitlabSecurity/PublicSend
|
57
57
|
end
|
58
|
+
# rubocop:enable GitlabSecurity/PublicSend
|
59
|
+
|
58
60
|
if old_op_value != return_value
|
59
61
|
@value = return_value
|
60
62
|
@op_value = return_value
|
61
63
|
end
|
64
|
+
|
62
65
|
@value
|
63
66
|
else
|
64
67
|
super
|
data/lib/nacha/parser.rb
CHANGED
@@ -1,13 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'byebug'
|
3
4
|
require 'nacha'
|
4
5
|
require 'nacha/parser_context'
|
5
6
|
|
6
7
|
# Nacha Parser - deal with figuring out what record type a line is
|
7
8
|
class Nacha::Parser
|
8
|
-
DEFAULT_RECORD_TYPES = [
|
9
|
-
|
10
|
-
|
9
|
+
DEFAULT_RECORD_TYPES = [
|
10
|
+
'Nacha::Record::FileHeader',
|
11
|
+
'Nacha::Record::AdvFileHeader',
|
12
|
+
'Nacha::Record::Filler'
|
13
|
+
].freeze
|
11
14
|
|
12
15
|
attr_reader :context
|
13
16
|
|
@@ -16,31 +19,25 @@ class Nacha::Parser
|
|
16
19
|
end
|
17
20
|
|
18
21
|
def parse_file(file)
|
19
|
-
@context.parser_started_at = Time.now
|
22
|
+
@context.parser_started_at = Time.now.utc
|
20
23
|
@context.file_name = file
|
21
24
|
parse_string(file.read)
|
22
25
|
end
|
23
26
|
|
24
27
|
def detect_possible_record_types(line)
|
25
|
-
Nacha.ach_record_types.
|
26
|
-
record_type if record_type.matcher
|
27
|
-
end.compact
|
28
|
-
end
|
29
|
-
|
30
|
-
def parse_line(line)
|
31
|
-
record_types = detect_possible_record_types(line)
|
32
|
-
|
33
|
-
records = record_types.map do |record_type|
|
34
|
-
|
28
|
+
Nacha.ach_record_types.filter_map do |record_type|
|
29
|
+
record_type if record_type.matcher.match?(line)
|
35
30
|
end
|
36
31
|
end
|
37
32
|
|
38
33
|
def parse_string(str)
|
39
34
|
line_num = -1
|
40
35
|
records = []
|
41
|
-
@context.parser_started_at ||= Time.now
|
42
|
-
str.scan(/(.{94}
|
43
|
-
line = line.first.strip
|
36
|
+
@context.parser_started_at ||= Time.now.utc
|
37
|
+
str.scan(/(.{0,94})[\r\n]*/).each do |line|
|
38
|
+
line = line.compact.first.strip
|
39
|
+
next if line.empty? || line.start_with?('#') # Skip empty lines and comments
|
40
|
+
|
44
41
|
line_num += 1
|
45
42
|
@context.line_number = line_num
|
46
43
|
@context.line_length = line.length
|
@@ -54,9 +51,11 @@ class Nacha::Parser
|
|
54
51
|
parent = previous
|
55
52
|
|
56
53
|
record_types = valid_record_types(parent)
|
54
|
+
|
57
55
|
while record_types
|
58
56
|
record = parse_first_by_types(line, record_types)
|
59
57
|
break if record || !parent
|
58
|
+
|
60
59
|
record.validate if record
|
61
60
|
parent = parent.parent
|
62
61
|
record_types = valid_record_types(parent)
|
@@ -80,20 +79,16 @@ class Nacha::Parser
|
|
80
79
|
end
|
81
80
|
|
82
81
|
def parse_first_by_types(line, record_types)
|
83
|
-
record_types.
|
82
|
+
record_types.lazy.filter_map do |rt|
|
84
83
|
record_type = rt.is_a?(Class) ? rt : Object.const_get(rt)
|
85
|
-
|
86
|
-
|
87
|
-
return record if record
|
88
|
-
end
|
84
|
+
record_type.parse(line) if record_type.matcher.match?(line)
|
85
|
+
end.first
|
89
86
|
end
|
90
87
|
|
91
88
|
def parse_all_by_types(line, record_types)
|
92
|
-
record_types.
|
89
|
+
record_types.filter_map do |rt|
|
93
90
|
record_type = rt.is_a?(Class) ? rt : Object.const_get(rt)
|
94
|
-
|
95
|
-
|
96
|
-
record
|
97
|
-
end.compact
|
91
|
+
record_type.parse(line) if record_type.matcher.match?(line)
|
92
|
+
end
|
98
93
|
end
|
99
94
|
end
|