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.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +63 -0
  3. data/.gitignore +2 -0
  4. data/.rubocop.yml +4 -2
  5. data/.ruby-version +1 -1
  6. data/CHANGELOG.md +42 -0
  7. data/README.md +10 -2
  8. data/exe/nacha +46 -29
  9. data/lib/nacha/aba_number.rb +33 -24
  10. data/lib/nacha/ach_date.rb +15 -8
  11. data/lib/nacha/field.rb +62 -77
  12. data/lib/nacha/formatter/base.rb +52 -0
  13. data/lib/nacha/formatter/html_formatter.rb +119 -0
  14. data/lib/nacha/formatter/json_formatter.rb +49 -0
  15. data/lib/nacha/formatter/markdown_formatter.rb +57 -0
  16. data/lib/nacha/formatter.rb +24 -0
  17. data/lib/nacha/has_errors.rb +12 -8
  18. data/lib/nacha/numeric.rb +13 -10
  19. data/lib/nacha/parser.rb +32 -31
  20. data/lib/nacha/parser_context.rb +4 -9
  21. data/lib/nacha/record/ack_entry_detail.rb +15 -8
  22. data/lib/nacha/record/addenda_record_type.rb +8 -1
  23. data/lib/nacha/record/adv_batch_control.rb +12 -7
  24. data/lib/nacha/record/adv_entry_detail.rb +3 -2
  25. data/lib/nacha/record/adv_file_control.rb +9 -5
  26. data/lib/nacha/record/adv_file_header.rb +11 -6
  27. data/lib/nacha/record/arc_entry_detail.rb +2 -2
  28. data/lib/nacha/record/base.rb +124 -106
  29. data/lib/nacha/record/batch_control.rb +13 -7
  30. data/lib/nacha/record/batch_header.rb +20 -11
  31. data/lib/nacha/record/batch_header_record_type.rb +5 -4
  32. data/lib/nacha/record/boc_entry_detail.rb +3 -2
  33. data/lib/nacha/record/ccd_addenda.rb +2 -2
  34. data/lib/nacha/record/ccd_entry_detail.rb +3 -2
  35. data/lib/nacha/record/cie_addenda.rb +2 -2
  36. data/lib/nacha/record/cie_entry_detail.rb +5 -3
  37. data/lib/nacha/record/ctx_addenda.rb +2 -2
  38. data/lib/nacha/record/ctx_corporate_entry_detail.rb +2 -2
  39. data/lib/nacha/record/detail_record_type.rb +4 -1
  40. data/lib/nacha/record/dne_addenda.rb +2 -2
  41. data/lib/nacha/record/dne_entry_detail.rb +6 -4
  42. data/lib/nacha/record/enr_addenda.rb +3 -2
  43. data/lib/nacha/record/enr_entry_detail.rb +4 -3
  44. data/lib/nacha/record/fifth_iat_addenda.rb +8 -4
  45. data/lib/nacha/record/file_control.rb +9 -5
  46. data/lib/nacha/record/file_control_record_type.rb +1 -1
  47. data/lib/nacha/record/file_header.rb +12 -8
  48. data/lib/nacha/record/file_header_record_type.rb +1 -1
  49. data/lib/nacha/record/filler.rb +3 -3
  50. data/lib/nacha/record/filler_record_type.rb +3 -1
  51. data/lib/nacha/record/first_iat_addenda.rb +4 -3
  52. data/lib/nacha/record/fourth_iat_addenda.rb +7 -4
  53. data/lib/nacha/record/iat_batch_header.rb +5 -3
  54. data/lib/nacha/record/iat_entry_detail.rb +9 -6
  55. data/lib/nacha/record/iat_foreign_coorespondent_bank_information_addenda.rb +10 -6
  56. data/lib/nacha/record/iat_remittance_information_addenda.rb +3 -2
  57. data/lib/nacha/record/mte_addenda.rb +4 -3
  58. data/lib/nacha/record/mte_entry_detail.rb +5 -3
  59. data/lib/nacha/record/pop_entry_detail.rb +3 -2
  60. data/lib/nacha/record/pos_addenda.rb +6 -3
  61. data/lib/nacha/record/pos_entry_detail.rb +5 -3
  62. data/lib/nacha/record/ppd_addenda.rb +3 -2
  63. data/lib/nacha/record/ppd_entry_detail.rb +5 -3
  64. data/lib/nacha/record/rck_entry_detail.rb +3 -2
  65. data/lib/nacha/record/second_iat_addenda.rb +3 -2
  66. data/lib/nacha/record/seventh_iat_addenda.rb +3 -2
  67. data/lib/nacha/record/shr_addenda.rb +5 -3
  68. data/lib/nacha/record/shr_entry_detail.rb +3 -2
  69. data/lib/nacha/record/sixth_iat_addenda.rb +5 -3
  70. data/lib/nacha/record/tel_entry_detail.rb +5 -3
  71. data/lib/nacha/record/third_iat_addenda.rb +3 -2
  72. data/lib/nacha/record/trc_entry_detail.rb +3 -2
  73. data/lib/nacha/record/trx_addenda.rb +3 -2
  74. data/lib/nacha/record/trx_entry_detail.rb +5 -3
  75. data/lib/nacha/record/validations/field_validations.rb +26 -14
  76. data/lib/nacha/record/validations/record_validations.rb +2 -1
  77. data/lib/nacha/record/web_addenda.rb +3 -2
  78. data/lib/nacha/record/web_entry_detail.rb +5 -3
  79. data/lib/nacha/record/xck_entry_detail.rb +3 -2
  80. data/lib/nacha/version.rb +4 -1
  81. data/lib/nacha.rb +21 -14
  82. data/nacha.gemspec +14 -16
  83. metadata +42 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6d8625f3af750aa7ba6e526909ad823f6020b761ab740616e82650f856585a13
4
- data.tar.gz: 459d7359527c582eddf7d46770b2527896d465db86be48a1f6a994bc8d15471d
3
+ metadata.gz: 986181f9dfa47368890ac283705b188bfef4733adff641590cc889f491cf5ed2
4
+ data.tar.gz: 1aff3dd6aedd49cf8f1e9d2562131b28832452fd0332a71429f49955aa19e858
5
5
  SHA512:
6
- metadata.gz: fb31a6b021d3a7b7752f079651bfc5d2f78d5623883ab91201e76853273c3002c1d4a0423d971d5c75a2c38b03faee3e31d64d7cf82cb28f632c4c4174a6526b
7
- data.tar.gz: 2aaf5af05547d0e0f10afb1f2a310e70b69e11c359979df5d238389c7800ab3f76ff163a5f35275162809f44b923603c2ed69658c96fe11e7f86eb8cbdf204cf
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
@@ -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
- Metrics/LineLength:
29
- Max: 92
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.4.4
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
+ [![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
@@ -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 'nacha' # Assuming this loads the Nacha gem
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
- "templates").freeze
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
- ach_file = [Nacha.parse(File.open(file_path)).first] # Use Nacha.parse
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
- output_html(file_path, ach_file)
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 output_html(file_path, ach_records)
39
- puts html_preamble
40
- puts "<h1>Successfully parsed #{file_path}</h1>\n"
41
- ach_records.each do |record|
42
- display_child(0, record)
43
- end
44
- puts html_postamble
45
- end
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
- def html_preamble
48
- @html_preamble ||= File.read(HTML_PREAMBLE_FILE)
49
- end
50
-
51
- def html_postamble
52
- @html_postamble ||= File.read(HTML_POSTAMBLE_FILE)
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 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
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
@@ -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
- self.routing_number = routing_number
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
- n = @routing_number.to_s.strip.ljust(8, '0').chars.collect(&:to_i)
18
- sum = (3 * (n[0] + n[3] + n[6])) +
19
- (7 * (n[1] + n[4] + n[7])) +
20
- (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
+
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
- @routing_number.chars[8] if @routing_number.length == 9 && compute_check_digit == @routing_number.chars[8]
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
- if @routing_number.length == 9
42
- valid_check_digit?
43
- else # 8 digits is valid
44
- true
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 #{actual_length} digits long.")
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
- if compute_check_digit != @routing_number.chars[8]
64
- add_error("Incorrect Check Digit \"#{@routing_number.chars[8]}\" should be \"#{ compute_check_digit }\" ")
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(with_checkdigit = true)
72
- if with_checkdigit
73
- @routing_number
74
- else
75
- @routing_number[0..7]
76
- end
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
@@ -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, month, day = temp_date.year, temp_date.month, temp_date.day
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, month, day = original_date.year, original_date.month, original_date.day
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
- if args.length == 3 && args.all? { |arg| arg.is_a?(Integer) }
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(*args) # IMPORTANT: Call super to create the instance
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(*args)
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}. Original error: #{e.message}"
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
- attr_accessor :name, :errors
12
- attr_reader :contents, :data, :input_data
13
- attr_reader :data_type
14
- attr_reader :validator
15
- attr_reader :justification
16
- attr_reader :fill_character
17
- attr_reader :output_conversion
18
- attr_reader :json_output
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
- if respond_to?(setter)
33
- send(setter, v) unless v.nil?
34
- end
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 ** (@contents.count('¢'))
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
- add_error("'inclusion' must be present for a field definition.") unless @inclusion
108
- add_error("'position' must be present for a field definition.") unless @position
109
- add_error("'contents' must be present for a field definition.") unless @contents
110
-
111
- if @data_assigned &&
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
- str.send(justification, position.count, fill_char)
164
- end
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(' ', '&nbsp;')
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
- @data.send(*output_conversion).to_s
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