Class: Nacha::Record::Base

Inherits:
Object
  • Object
show all
Includes:
Validations::FieldValidations
Defined in:
lib/nacha/record/base.rb

Overview

Base class for all Nacha records.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Validations::FieldValidations

included

Constructor Details

#initialize(opts = {}) ⇒ Base

Initializes a new record object.

Parameters:

  • opts (Hash) (defaults to: {})

    A hash of options to set as attributes on the new record.



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/nacha/record/base.rb', line 34

def initialize(opts = {})
  @children = []
  @parent = nil
  @errors = []
  @original_input_line = nil
  @line_number = nil
  @dirty = false
  @fields = {}
  create_fields_from_definition
  opts.each do |key, value|
    setter = "#{key}="
    # rubocop:disable GitlabSecurity/PublicSend
    public_send(setter, value) if value && respond_to?(setter)
    # rubocop:enable GitlabSecurity/PublicSend
  end
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method_name, *args, &block) ⇒ Nacha::Field, Object

Handles dynamic access to fields as methods.

This allows getting and setting field data using attribute-style methods (e.g., ‘record.amount`, `record.amount = 123.45`).

Parameters:

  • method_name (Symbol)

    The name of the missing method.

  • args (Array)

    The arguments passed to the method.

  • block (Proc)

    A block passed to the method.

Returns:

  • (Nacha::Field, Object)

    Returns the ‘Nacha::Field` object for a getter, or the assigned value for a setter.



413
414
415
416
417
418
419
420
421
422
423
424
# File 'lib/nacha/record/base.rb', line 413

def method_missing(method_name, *args, &block)
  method = method_name.to_s
  field_name = method.gsub(/=$/, '').to_sym
  field = @fields[field_name]
  return super unless field

  if method.end_with?('=')
    assign_field_data(field, args)
  else
    field
  end
end

Instance Attribute Details

#childrenArray<Nacha::Record::Base> (readonly)

Returns An array of child records associated with this record.

Returns:



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
# File 'lib/nacha/record/base.rb', line 21

class Base
  include Validations::FieldValidations

  attr_reader :children, :name, :validations, :original_input_line, :fields

  # :reek:Attribute

  attr_accessor :parent, :line_number

  # :reek:ManualDispatch

  # Initializes a new record object.
  # @param opts [Hash] A hash of options to set as attributes on the new record.
  def initialize(opts = {})
    @children = []
    @parent = nil
    @errors = []
    @original_input_line = nil
    @line_number = nil
    @dirty = false
    @fields = {}
    create_fields_from_definition
    opts.each do |key, value|
      setter = "#{key}="
      # rubocop:disable GitlabSecurity/PublicSend
      public_send(setter, value) if value && respond_to?(setter)
      # rubocop:enable GitlabSecurity/PublicSend
    end
  end

  class << self
    # :reek:LongParameterList, :reek:ManualDispatch
    # Defines a field for the record type.
    #
    # This class method is used in subclasses to declare each field within the NACHA record.
    # It stores the definition and sets up validations.
    #
    # @param name [Symbol] The name of the field.
    # @param inclusion [String] The inclusion rule for the field ('M' for mandatory, 'R' for required, 'O' for optional).
    # @param contents [String] The expected content type of the field (e.g., 'Numeric', 'Alphameric', 'C...').
    # @param position [Range] The character position range of the field in the record string.
    # @return [void]
    def nacha_field(name, inclusion:, contents:, position:)
      Nacha.add_ach_record_type(self)
      definition[name] = { inclusion: inclusion,
                           contents: contents,
                           position: position,
                           name: name }
      validation_method = :"valid_#{name}"
      return unless respond_to?(validation_method)

      (validations[name] ||= []) << validation_method
    end

    # Returns the field definitions for the record class.
    # @return [Hash] A hash containing the definitions of all fields for this record type.
    def definition
      @definition ||= {}
    end

    # Returns the validation methods for the record class.
    # @return [Hash] A hash of validation methods for the fields of this record type.
    def validations
      @validations ||= {}
    end

    # Generates and returns a format string for `String#unpack` to parse a record line.
    #
    # The format string is built based on the field definitions.
    # @return [String] The unpack format string.
    def unpack_str
      @unpack_str ||= definition.values
                        .sort { |a, b| a[:position].first <=> b[:position].first }
                        .collect do |field_def|
        Nacha::Field.unpack_str(field_def)
      end.join.freeze
    end

    # :reek:TooManyStatements
    # rubocop:disable Layout/BlockAlignment,
    # rubocop:disable Style/StringConcatenation:

    # Generates a Regexp to match against a line to determine if it is of this record type.
    # @return [Regexp] The regular expression for matching record lines.
    def matcher
      @matcher ||= begin
        output_started = false
        skipped_output = false
        Regexp.new('\A' + definition.values
                            .sort { |a,b| a[:position].first <=> b[:position].first }.reverse.collect do |field|
                     contents = field[:contents]
                     position = field[:position]
                     size = position.size
                     if contents =~ /\AC(.+)\z/
                       last_match = Regexp.last_match(1)
                       if last_match.match?(/  */) && !output_started
                         skipped_output = true
                         ''
                       else
                         output_started = true
                         last_match
                       end
                     elsif contents.match?(/\ANumeric\z/) || contents.match?(/\AYYMMDD\z/)
                       output_started = true
                       "[0-9 ]{#{size}}"
                     elsif output_started
                       ".{#{size}}"
                     else
                       skipped_output = true
                       ''
                     end
                   end.reverse.join + (skipped_output ? '.*' : '') + '\z')
      end
    end
    # rubocop:enable Layout/BlockAlignment,
    # rubocop:enable Style/StringConcatenation:

    # Parses an ACH string and creates a new record instance.
    #
    # It unpacks the string based on field definitions, populates the fields,
    # and runs validations.
    # @param ach_str [String] The 94-character string representing a NACHA record.
    # @return [Nacha::Record::Base] A new instance of the record class populated with data from the string.
    def parse(ach_str)
      rec = new(original_input_line: ach_str)
      ach_str.unpack(unpack_str).zip(rec.fields.values) do |input_data, field|
        field.data = input_data
      end
      rec.validate
      rec
    end

    # Returns the record type name.
    #
    # The name is derived from the class name, converted to a snake_case symbol.
    # @return [Symbol] The record type as a symbol (e.g., :file_header).
    def record_type
      Nacha.record_name(self)
    end

    # Returns a hash representation of the record class definition.
    #
    # This includes field definitions and child record types.
    # @return [Hash] A hash representing the structure of the record type.
    def to_h # :reek:ManualDispatch, :reek:TooManyStatements
      fields = definition.transform_values do |field_def|
        {
          inclusion: field_def[:inclusion],
          contents: field_def[:contents],
          position: field_def[:position].to_s
        }
      end

      fields[:child_record_types] = child_record_types.to_a
      fields[:child_record_types] ||= []
      fields[:klass] = name.to_s

      { record_type.to_sym => fields }
    end

    # Returns a JSON string representing the record class definition.
    # @param _args [Array] Arguments to be passed to `JSON.pretty_generate`.
    # @return [String] The JSON representation of the record definition.
    def to_json(*_args)
      JSON.pretty_generate(to_h)
    end
  end

  # :reek:FeatureEnvy

  # Sets the original input line string for the record.
  # @param line [String] The original 94-character string from the ACH file.
  # @return [void]
  def original_input_line=(line)
    @original_input_line = line.dup if line.is_a?(String)
  end

  # Creates field instances from the class's field definitions.
  #
  # This method is called during initialization to populate the `@fields` hash.
  # @return [void]
  def create_fields_from_definition
    definition.each_pair do |field_name, field_def|
      @fields[field_name.to_sym] = Nacha::Field.new(field_def)
    end
  end

  # Returns the record type name for the instance.
  # @return [Symbol] The record type as a symbol.
  # @see .record_type
  def record_type
    self.class.record_type
  end

  # Returns a human-readable name for the record type.
  #
  # It converts the snake_case `record_type` into a capitalized string with spaces.
  # @return [String] The human-readable name (e.g., 'File Header').
  def human_name
    @human_name ||= record_type.to_s.split('_').map(&:capitalize).join(' ')
  end

  # Returns a hash representation of the record instance.
  #
  # The hash includes metadata and a representation of each field's data and errors.
  # @return [Hash] A hash representing the record's current state.
  def to_h
    { nacha_record_type: record_type,
      metadata: {
        klass: self.class.name,
        errors: errors,
        line_number: @line_number,
        original_input_line: original_input_line
      } }.merge(
        @fields.keys.to_h do |key|
          [key, @fields[key].to_json_output]
        end)
  end

  # Returns a JSON string representing the record instance.
  # @param _args [Array] Arguments to be passed to `JSON.pretty_generate`.
  # @return [String] The JSON representation of the record's current state.
  def to_json(*_args)
    JSON.pretty_generate(to_h)
  end

  # Generates the 94-character ACH string representation of the record.
  #
  # This is done by concatenating the ACH string representation of each field.
  # @return [String] The 94-character ACH record string.
  def to_ach
    @fields.keys.collect do |key|
      @fields[key].to_ach
    end.join
  end

  # Generates an HTML representation of the record.
  #
  # This is useful for displaying ACH data in a web interface.
  # @param _opts [Hash] Options for HTML generation (currently unused).
  # @return [String] The HTML representation of the record.
  def to_html(_opts = {})
    record_error_class = nil

    field_html = @fields.values.collect do |field|
      record_error_class ||= 'error' if field.errors.any?
      field.to_html
    end.join
    "<div class=\"nacha-record tooltip #{record_type} #{record_error_class}\">" \
      "<span class=\"nacha-record-number\" data-name=\"record-number\">#{format('%05d',
        line_number)}&nbsp;|&nbsp</span>" \
      "#{field_html}" \
      "<span class=\"record-type\" data-name=\"record-type\">#{human_name}</span>" \
      "</div>"
  end

  # Returns a developer-friendly string representation of the record object.
  # @return [String] A string showing the class name and a hash representation of the object.
  def inspect
    "#<#{self.class.name}> #{to_h}"
  end

  # Returns the field definitions for the record class.
  # @return [Hash] A hash containing the definitions of all fields for this record type.
  # @see .definition
  def definition
    self.class.definition
  end

  # Runs all field-level and record-level validations.
  #
  # This method populates the `errors` array on the record and its fields.
  # @return [void]
  def validate
    # Run field-level validations first
    @fields.each_value(&:validate)
    # Then run record-level validations that might depend on multiple fields
    self.class.definition.each_key do |field|
      run_record_level_validations_for(field)
    end
  end

  # Checks if the record is valid by running all validations.
  # @return [Boolean] `true` if the record has no errors, `false` otherwise.
  def valid?
    validate
    errors.empty?
  end

  # Checks if the current transaction code represents a debit transaction.
  #
  # This method evaluates the `transaction_code` (which is expected to be
  # an attribute or method available in the current context) against a
  # predefined set of debit transaction codes.
  #
  # @return [Boolean] `true` if `transaction_code` is present and its
  #   string representation is included in `DEBIT_TRANSACTION_CODES`,
  #   `false` otherwise.
  #
  # @example
  #   # Assuming transaction_code is "201" and DEBIT_TRANSACTION_CODES includes "201"
  #   debit? #=> true
  #
  #   # Assuming transaction_code is "100" and DEBIT_TRANSACTION_CODES
  #   # does not include "100"
  #   debit? #=> false
  #
  #   # Assuming transaction_code is nil
  #   debit? #=> false
  #
  # @note
  #   This method includes robust error handling. If a `NoMethodError`
  #   occurs (e.g., if `transaction_code` is undefinable or does not respond
  #   to `to_s` in an unexpected way) or a `NameError` occurs (e.g., if
  #   `transaction_code` or `DEBIT_TRANSACTION_CODES` is not defined
  #   in the current scope), the method gracefully rescues these exceptions
  #   and returns `false`. This default behavior ensures that an inability
  #   to determine the transaction type results in it being considered
  #   "not a debit".
  #
  # @see #transaction_code (if `transaction_code` is an instance method or attribute)
  # @see DEBIT_TRANSACTION_CODES (the constant defining debit codes)
  def debit?
    transaction_code &&
      DEBIT_TRANSACTION_CODES.include?(transaction_code.to_s)
  rescue NoMethodError, NameError
    false
  end

  # Checks if the current transaction code represents a credit transaction.
  #
  # This method evaluates the `transaction_code` (which is expected to be
  # an attribute or method available in the current context) against a
  # predefined set of credit transaction codes.
  #
  # @return [Boolean] `true` if `transaction_code` is present and its
  #   string representation is included in `CREDIT_TRANSACTION_CODES`,
  #   `false` otherwise.
  #
  # @example
  #   # Assuming transaction_code is "101" and CREDIT_TRANSACTION_CODES includes "101"
  #   credit? #=> true
  #
  #   # Assuming transaction_code is "200" and CREDIT_TRANSACTION_CODES
  #   # does not include "200"
  #   credit? #=> false
  #
  #   # Assuming transaction_code is nil
  #   credit? #=> false
  #
  # @note
  #   This method includes robust error handling. If a `NoMethodError`
  #   occurs (e.g., if `transaction_code` is undefinable or does not respond
  #   to `to_s` in an unexpected way) or a `NameError` occurs (e.g., if
  #   `transaction_code` or `CREDIT_TRANSACTION_CODES` is not defined
  #   in the current scope), the method gracefully rescues these exceptions
  #   and returns `false`. This default behavior ensures that an inability
  #   to determine the transaction type results in it being considered
  #   "not a credit".
  #
  # @see #transaction_code (if `transaction_code` is an instance method or attribute)
  # @see CREDIT_TRANSACTION_CODES (the constant defining credit codes)
  def credit?
    transaction_code &&
      CREDIT_TRANSACTION_CODES.include?(transaction_code.to_s)
  rescue NoMethodError, NameError
    false
  end

  # Returns all validation errors for the record and its fields.
  # @return [Array<String>] An array of error messages.
  def errors
    (@errors + @fields.values.map(&:errors)).flatten
  end

  # Adds a validation error message to the record's error list.
  # @param err_string [String] The error message to add.
  # @return [void]
  def add_error(err_string)
    @errors << err_string
  end

  # :reek:TooManyStatements

  # Handles dynamic access to fields as methods.
  #
  # This allows getting and setting field data using attribute-style methods
  # (e.g., `record.amount`, `record.amount = 123.45`).
  # @param method_name [Symbol] The name of the missing method.
  # @param args [Array] The arguments passed to the method.
  # @param block [Proc] A block passed to the method.
  # @return [Nacha::Field, Object] Returns the `Nacha::Field` object for a getter, or the assigned value for a setter.
  def method_missing(method_name, *args, &block)
    method = method_name.to_s
    field_name = method.gsub(/=$/, '').to_sym
    field = @fields[field_name]
    return super unless field

    if method.end_with?('=')
      assign_field_data(field, args)
    else
      field
    end
  end

  # Complements `method_missing` to allow `respond_to?` to work for dynamic field methods.
  # @param method_name [Symbol] The name of the method to check.
  # @param _ [Array] Additional arguments (e.g. `include_private`) are ignored.
  # @return [Boolean] `true` if the method corresponds to a defined field, `false` otherwise.
  def respond_to_missing?(method_name, *)
    field_name = method_name.to_s.gsub(/=$/, '').to_sym
    definition[field_name] || super
  end

  private

  def assign_field_data(field, args)
    # rubocop:disable GitlabSecurity/PublicSend
    field.public_send(:data=, *args)
    # rubocop:enable GitlabSecurity/PublicSend
    @dirty = true
  end

  # :reek:TooManyStatements

  def run_record_level_validations_for(field)
    klass = self.class
    validations = klass.validations[field]
    return unless validations

    # rubocop:disable GitlabSecurity/PublicSend
    field_data = send(field)

    validations.each do |validation_method|
      klass.send(validation_method, field_data)
    end
    # rubocop:enable GitlabSecurity/PublicSend
  end
end

#fieldsHash{Symbol => Nacha::Field} (readonly)

Returns A hash of field objects that belong to this record, keyed by field name.

Returns:

  • (Hash{Symbol => Nacha::Field})

    A hash of field objects that belong to this record, keyed by field name.



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
# File 'lib/nacha/record/base.rb', line 21

class Base
  include Validations::FieldValidations

  attr_reader :children, :name, :validations, :original_input_line, :fields

  # :reek:Attribute

  attr_accessor :parent, :line_number

  # :reek:ManualDispatch

  # Initializes a new record object.
  # @param opts [Hash] A hash of options to set as attributes on the new record.
  def initialize(opts = {})
    @children = []
    @parent = nil
    @errors = []
    @original_input_line = nil
    @line_number = nil
    @dirty = false
    @fields = {}
    create_fields_from_definition
    opts.each do |key, value|
      setter = "#{key}="
      # rubocop:disable GitlabSecurity/PublicSend
      public_send(setter, value) if value && respond_to?(setter)
      # rubocop:enable GitlabSecurity/PublicSend
    end
  end

  class << self
    # :reek:LongParameterList, :reek:ManualDispatch
    # Defines a field for the record type.
    #
    # This class method is used in subclasses to declare each field within the NACHA record.
    # It stores the definition and sets up validations.
    #
    # @param name [Symbol] The name of the field.
    # @param inclusion [String] The inclusion rule for the field ('M' for mandatory, 'R' for required, 'O' for optional).
    # @param contents [String] The expected content type of the field (e.g., 'Numeric', 'Alphameric', 'C...').
    # @param position [Range] The character position range of the field in the record string.
    # @return [void]
    def nacha_field(name, inclusion:, contents:, position:)
      Nacha.add_ach_record_type(self)
      definition[name] = { inclusion: inclusion,
                           contents: contents,
                           position: position,
                           name: name }
      validation_method = :"valid_#{name}"
      return unless respond_to?(validation_method)

      (validations[name] ||= []) << validation_method
    end

    # Returns the field definitions for the record class.
    # @return [Hash] A hash containing the definitions of all fields for this record type.
    def definition
      @definition ||= {}
    end

    # Returns the validation methods for the record class.
    # @return [Hash] A hash of validation methods for the fields of this record type.
    def validations
      @validations ||= {}
    end

    # Generates and returns a format string for `String#unpack` to parse a record line.
    #
    # The format string is built based on the field definitions.
    # @return [String] The unpack format string.
    def unpack_str
      @unpack_str ||= definition.values
                        .sort { |a, b| a[:position].first <=> b[:position].first }
                        .collect do |field_def|
        Nacha::Field.unpack_str(field_def)
      end.join.freeze
    end

    # :reek:TooManyStatements
    # rubocop:disable Layout/BlockAlignment,
    # rubocop:disable Style/StringConcatenation:

    # Generates a Regexp to match against a line to determine if it is of this record type.
    # @return [Regexp] The regular expression for matching record lines.
    def matcher
      @matcher ||= begin
        output_started = false
        skipped_output = false
        Regexp.new('\A' + definition.values
                            .sort { |a,b| a[:position].first <=> b[:position].first }.reverse.collect do |field|
                     contents = field[:contents]
                     position = field[:position]
                     size = position.size
                     if contents =~ /\AC(.+)\z/
                       last_match = Regexp.last_match(1)
                       if last_match.match?(/  */) && !output_started
                         skipped_output = true
                         ''
                       else
                         output_started = true
                         last_match
                       end
                     elsif contents.match?(/\ANumeric\z/) || contents.match?(/\AYYMMDD\z/)
                       output_started = true
                       "[0-9 ]{#{size}}"
                     elsif output_started
                       ".{#{size}}"
                     else
                       skipped_output = true
                       ''
                     end
                   end.reverse.join + (skipped_output ? '.*' : '') + '\z')
      end
    end
    # rubocop:enable Layout/BlockAlignment,
    # rubocop:enable Style/StringConcatenation:

    # Parses an ACH string and creates a new record instance.
    #
    # It unpacks the string based on field definitions, populates the fields,
    # and runs validations.
    # @param ach_str [String] The 94-character string representing a NACHA record.
    # @return [Nacha::Record::Base] A new instance of the record class populated with data from the string.
    def parse(ach_str)
      rec = new(original_input_line: ach_str)
      ach_str.unpack(unpack_str).zip(rec.fields.values) do |input_data, field|
        field.data = input_data
      end
      rec.validate
      rec
    end

    # Returns the record type name.
    #
    # The name is derived from the class name, converted to a snake_case symbol.
    # @return [Symbol] The record type as a symbol (e.g., :file_header).
    def record_type
      Nacha.record_name(self)
    end

    # Returns a hash representation of the record class definition.
    #
    # This includes field definitions and child record types.
    # @return [Hash] A hash representing the structure of the record type.
    def to_h # :reek:ManualDispatch, :reek:TooManyStatements
      fields = definition.transform_values do |field_def|
        {
          inclusion: field_def[:inclusion],
          contents: field_def[:contents],
          position: field_def[:position].to_s
        }
      end

      fields[:child_record_types] = child_record_types.to_a
      fields[:child_record_types] ||= []
      fields[:klass] = name.to_s

      { record_type.to_sym => fields }
    end

    # Returns a JSON string representing the record class definition.
    # @param _args [Array] Arguments to be passed to `JSON.pretty_generate`.
    # @return [String] The JSON representation of the record definition.
    def to_json(*_args)
      JSON.pretty_generate(to_h)
    end
  end

  # :reek:FeatureEnvy

  # Sets the original input line string for the record.
  # @param line [String] The original 94-character string from the ACH file.
  # @return [void]
  def original_input_line=(line)
    @original_input_line = line.dup if line.is_a?(String)
  end

  # Creates field instances from the class's field definitions.
  #
  # This method is called during initialization to populate the `@fields` hash.
  # @return [void]
  def create_fields_from_definition
    definition.each_pair do |field_name, field_def|
      @fields[field_name.to_sym] = Nacha::Field.new(field_def)
    end
  end

  # Returns the record type name for the instance.
  # @return [Symbol] The record type as a symbol.
  # @see .record_type
  def record_type
    self.class.record_type
  end

  # Returns a human-readable name for the record type.
  #
  # It converts the snake_case `record_type` into a capitalized string with spaces.
  # @return [String] The human-readable name (e.g., 'File Header').
  def human_name
    @human_name ||= record_type.to_s.split('_').map(&:capitalize).join(' ')
  end

  # Returns a hash representation of the record instance.
  #
  # The hash includes metadata and a representation of each field's data and errors.
  # @return [Hash] A hash representing the record's current state.
  def to_h
    { nacha_record_type: record_type,
      metadata: {
        klass: self.class.name,
        errors: errors,
        line_number: @line_number,
        original_input_line: original_input_line
      } }.merge(
        @fields.keys.to_h do |key|
          [key, @fields[key].to_json_output]
        end)
  end

  # Returns a JSON string representing the record instance.
  # @param _args [Array] Arguments to be passed to `JSON.pretty_generate`.
  # @return [String] The JSON representation of the record's current state.
  def to_json(*_args)
    JSON.pretty_generate(to_h)
  end

  # Generates the 94-character ACH string representation of the record.
  #
  # This is done by concatenating the ACH string representation of each field.
  # @return [String] The 94-character ACH record string.
  def to_ach
    @fields.keys.collect do |key|
      @fields[key].to_ach
    end.join
  end

  # Generates an HTML representation of the record.
  #
  # This is useful for displaying ACH data in a web interface.
  # @param _opts [Hash] Options for HTML generation (currently unused).
  # @return [String] The HTML representation of the record.
  def to_html(_opts = {})
    record_error_class = nil

    field_html = @fields.values.collect do |field|
      record_error_class ||= 'error' if field.errors.any?
      field.to_html
    end.join
    "<div class=\"nacha-record tooltip #{record_type} #{record_error_class}\">" \
      "<span class=\"nacha-record-number\" data-name=\"record-number\">#{format('%05d',
        line_number)}&nbsp;|&nbsp</span>" \
      "#{field_html}" \
      "<span class=\"record-type\" data-name=\"record-type\">#{human_name}</span>" \
      "</div>"
  end

  # Returns a developer-friendly string representation of the record object.
  # @return [String] A string showing the class name and a hash representation of the object.
  def inspect
    "#<#{self.class.name}> #{to_h}"
  end

  # Returns the field definitions for the record class.
  # @return [Hash] A hash containing the definitions of all fields for this record type.
  # @see .definition
  def definition
    self.class.definition
  end

  # Runs all field-level and record-level validations.
  #
  # This method populates the `errors` array on the record and its fields.
  # @return [void]
  def validate
    # Run field-level validations first
    @fields.each_value(&:validate)
    # Then run record-level validations that might depend on multiple fields
    self.class.definition.each_key do |field|
      run_record_level_validations_for(field)
    end
  end

  # Checks if the record is valid by running all validations.
  # @return [Boolean] `true` if the record has no errors, `false` otherwise.
  def valid?
    validate
    errors.empty?
  end

  # Checks if the current transaction code represents a debit transaction.
  #
  # This method evaluates the `transaction_code` (which is expected to be
  # an attribute or method available in the current context) against a
  # predefined set of debit transaction codes.
  #
  # @return [Boolean] `true` if `transaction_code` is present and its
  #   string representation is included in `DEBIT_TRANSACTION_CODES`,
  #   `false` otherwise.
  #
  # @example
  #   # Assuming transaction_code is "201" and DEBIT_TRANSACTION_CODES includes "201"
  #   debit? #=> true
  #
  #   # Assuming transaction_code is "100" and DEBIT_TRANSACTION_CODES
  #   # does not include "100"
  #   debit? #=> false
  #
  #   # Assuming transaction_code is nil
  #   debit? #=> false
  #
  # @note
  #   This method includes robust error handling. If a `NoMethodError`
  #   occurs (e.g., if `transaction_code` is undefinable or does not respond
  #   to `to_s` in an unexpected way) or a `NameError` occurs (e.g., if
  #   `transaction_code` or `DEBIT_TRANSACTION_CODES` is not defined
  #   in the current scope), the method gracefully rescues these exceptions
  #   and returns `false`. This default behavior ensures that an inability
  #   to determine the transaction type results in it being considered
  #   "not a debit".
  #
  # @see #transaction_code (if `transaction_code` is an instance method or attribute)
  # @see DEBIT_TRANSACTION_CODES (the constant defining debit codes)
  def debit?
    transaction_code &&
      DEBIT_TRANSACTION_CODES.include?(transaction_code.to_s)
  rescue NoMethodError, NameError
    false
  end

  # Checks if the current transaction code represents a credit transaction.
  #
  # This method evaluates the `transaction_code` (which is expected to be
  # an attribute or method available in the current context) against a
  # predefined set of credit transaction codes.
  #
  # @return [Boolean] `true` if `transaction_code` is present and its
  #   string representation is included in `CREDIT_TRANSACTION_CODES`,
  #   `false` otherwise.
  #
  # @example
  #   # Assuming transaction_code is "101" and CREDIT_TRANSACTION_CODES includes "101"
  #   credit? #=> true
  #
  #   # Assuming transaction_code is "200" and CREDIT_TRANSACTION_CODES
  #   # does not include "200"
  #   credit? #=> false
  #
  #   # Assuming transaction_code is nil
  #   credit? #=> false
  #
  # @note
  #   This method includes robust error handling. If a `NoMethodError`
  #   occurs (e.g., if `transaction_code` is undefinable or does not respond
  #   to `to_s` in an unexpected way) or a `NameError` occurs (e.g., if
  #   `transaction_code` or `CREDIT_TRANSACTION_CODES` is not defined
  #   in the current scope), the method gracefully rescues these exceptions
  #   and returns `false`. This default behavior ensures that an inability
  #   to determine the transaction type results in it being considered
  #   "not a credit".
  #
  # @see #transaction_code (if `transaction_code` is an instance method or attribute)
  # @see CREDIT_TRANSACTION_CODES (the constant defining credit codes)
  def credit?
    transaction_code &&
      CREDIT_TRANSACTION_CODES.include?(transaction_code.to_s)
  rescue NoMethodError, NameError
    false
  end

  # Returns all validation errors for the record and its fields.
  # @return [Array<String>] An array of error messages.
  def errors
    (@errors + @fields.values.map(&:errors)).flatten
  end

  # Adds a validation error message to the record's error list.
  # @param err_string [String] The error message to add.
  # @return [void]
  def add_error(err_string)
    @errors << err_string
  end

  # :reek:TooManyStatements

  # Handles dynamic access to fields as methods.
  #
  # This allows getting and setting field data using attribute-style methods
  # (e.g., `record.amount`, `record.amount = 123.45`).
  # @param method_name [Symbol] The name of the missing method.
  # @param args [Array] The arguments passed to the method.
  # @param block [Proc] A block passed to the method.
  # @return [Nacha::Field, Object] Returns the `Nacha::Field` object for a getter, or the assigned value for a setter.
  def method_missing(method_name, *args, &block)
    method = method_name.to_s
    field_name = method.gsub(/=$/, '').to_sym
    field = @fields[field_name]
    return super unless field

    if method.end_with?('=')
      assign_field_data(field, args)
    else
      field
    end
  end

  # Complements `method_missing` to allow `respond_to?` to work for dynamic field methods.
  # @param method_name [Symbol] The name of the method to check.
  # @param _ [Array] Additional arguments (e.g. `include_private`) are ignored.
  # @return [Boolean] `true` if the method corresponds to a defined field, `false` otherwise.
  def respond_to_missing?(method_name, *)
    field_name = method_name.to_s.gsub(/=$/, '').to_sym
    definition[field_name] || super
  end

  private

  def assign_field_data(field, args)
    # rubocop:disable GitlabSecurity/PublicSend
    field.public_send(:data=, *args)
    # rubocop:enable GitlabSecurity/PublicSend
    @dirty = true
  end

  # :reek:TooManyStatements

  def run_record_level_validations_for(field)
    klass = self.class
    validations = klass.validations[field]
    return unless validations

    # rubocop:disable GitlabSecurity/PublicSend
    field_data = send(field)

    validations.each do |validation_method|
      klass.send(validation_method, field_data)
    end
    # rubocop:enable GitlabSecurity/PublicSend
  end
end

#line_numberObject

:reek:Attribute



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
# File 'lib/nacha/record/base.rb', line 21

class Base
  include Validations::FieldValidations

  attr_reader :children, :name, :validations, :original_input_line, :fields

  # :reek:Attribute

  attr_accessor :parent, :line_number

  # :reek:ManualDispatch

  # Initializes a new record object.
  # @param opts [Hash] A hash of options to set as attributes on the new record.
  def initialize(opts = {})
    @children = []
    @parent = nil
    @errors = []
    @original_input_line = nil
    @line_number = nil
    @dirty = false
    @fields = {}
    create_fields_from_definition
    opts.each do |key, value|
      setter = "#{key}="
      # rubocop:disable GitlabSecurity/PublicSend
      public_send(setter, value) if value && respond_to?(setter)
      # rubocop:enable GitlabSecurity/PublicSend
    end
  end

  class << self
    # :reek:LongParameterList, :reek:ManualDispatch
    # Defines a field for the record type.
    #
    # This class method is used in subclasses to declare each field within the NACHA record.
    # It stores the definition and sets up validations.
    #
    # @param name [Symbol] The name of the field.
    # @param inclusion [String] The inclusion rule for the field ('M' for mandatory, 'R' for required, 'O' for optional).
    # @param contents [String] The expected content type of the field (e.g., 'Numeric', 'Alphameric', 'C...').
    # @param position [Range] The character position range of the field in the record string.
    # @return [void]
    def nacha_field(name, inclusion:, contents:, position:)
      Nacha.add_ach_record_type(self)
      definition[name] = { inclusion: inclusion,
                           contents: contents,
                           position: position,
                           name: name }
      validation_method = :"valid_#{name}"
      return unless respond_to?(validation_method)

      (validations[name] ||= []) << validation_method
    end

    # Returns the field definitions for the record class.
    # @return [Hash] A hash containing the definitions of all fields for this record type.
    def definition
      @definition ||= {}
    end

    # Returns the validation methods for the record class.
    # @return [Hash] A hash of validation methods for the fields of this record type.
    def validations
      @validations ||= {}
    end

    # Generates and returns a format string for `String#unpack` to parse a record line.
    #
    # The format string is built based on the field definitions.
    # @return [String] The unpack format string.
    def unpack_str
      @unpack_str ||= definition.values
                        .sort { |a, b| a[:position].first <=> b[:position].first }
                        .collect do |field_def|
        Nacha::Field.unpack_str(field_def)
      end.join.freeze
    end

    # :reek:TooManyStatements
    # rubocop:disable Layout/BlockAlignment,
    # rubocop:disable Style/StringConcatenation:

    # Generates a Regexp to match against a line to determine if it is of this record type.
    # @return [Regexp] The regular expression for matching record lines.
    def matcher
      @matcher ||= begin
        output_started = false
        skipped_output = false
        Regexp.new('\A' + definition.values
                            .sort { |a,b| a[:position].first <=> b[:position].first }.reverse.collect do |field|
                     contents = field[:contents]
                     position = field[:position]
                     size = position.size
                     if contents =~ /\AC(.+)\z/
                       last_match = Regexp.last_match(1)
                       if last_match.match?(/  */) && !output_started
                         skipped_output = true
                         ''
                       else
                         output_started = true
                         last_match
                       end
                     elsif contents.match?(/\ANumeric\z/) || contents.match?(/\AYYMMDD\z/)
                       output_started = true
                       "[0-9 ]{#{size}}"
                     elsif output_started
                       ".{#{size}}"
                     else
                       skipped_output = true
                       ''
                     end
                   end.reverse.join + (skipped_output ? '.*' : '') + '\z')
      end
    end
    # rubocop:enable Layout/BlockAlignment,
    # rubocop:enable Style/StringConcatenation:

    # Parses an ACH string and creates a new record instance.
    #
    # It unpacks the string based on field definitions, populates the fields,
    # and runs validations.
    # @param ach_str [String] The 94-character string representing a NACHA record.
    # @return [Nacha::Record::Base] A new instance of the record class populated with data from the string.
    def parse(ach_str)
      rec = new(original_input_line: ach_str)
      ach_str.unpack(unpack_str).zip(rec.fields.values) do |input_data, field|
        field.data = input_data
      end
      rec.validate
      rec
    end

    # Returns the record type name.
    #
    # The name is derived from the class name, converted to a snake_case symbol.
    # @return [Symbol] The record type as a symbol (e.g., :file_header).
    def record_type
      Nacha.record_name(self)
    end

    # Returns a hash representation of the record class definition.
    #
    # This includes field definitions and child record types.
    # @return [Hash] A hash representing the structure of the record type.
    def to_h # :reek:ManualDispatch, :reek:TooManyStatements
      fields = definition.transform_values do |field_def|
        {
          inclusion: field_def[:inclusion],
          contents: field_def[:contents],
          position: field_def[:position].to_s
        }
      end

      fields[:child_record_types] = child_record_types.to_a
      fields[:child_record_types] ||= []
      fields[:klass] = name.to_s

      { record_type.to_sym => fields }
    end

    # Returns a JSON string representing the record class definition.
    # @param _args [Array] Arguments to be passed to `JSON.pretty_generate`.
    # @return [String] The JSON representation of the record definition.
    def to_json(*_args)
      JSON.pretty_generate(to_h)
    end
  end

  # :reek:FeatureEnvy

  # Sets the original input line string for the record.
  # @param line [String] The original 94-character string from the ACH file.
  # @return [void]
  def original_input_line=(line)
    @original_input_line = line.dup if line.is_a?(String)
  end

  # Creates field instances from the class's field definitions.
  #
  # This method is called during initialization to populate the `@fields` hash.
  # @return [void]
  def create_fields_from_definition
    definition.each_pair do |field_name, field_def|
      @fields[field_name.to_sym] = Nacha::Field.new(field_def)
    end
  end

  # Returns the record type name for the instance.
  # @return [Symbol] The record type as a symbol.
  # @see .record_type
  def record_type
    self.class.record_type
  end

  # Returns a human-readable name for the record type.
  #
  # It converts the snake_case `record_type` into a capitalized string with spaces.
  # @return [String] The human-readable name (e.g., 'File Header').
  def human_name
    @human_name ||= record_type.to_s.split('_').map(&:capitalize).join(' ')
  end

  # Returns a hash representation of the record instance.
  #
  # The hash includes metadata and a representation of each field's data and errors.
  # @return [Hash] A hash representing the record's current state.
  def to_h
    { nacha_record_type: record_type,
      metadata: {
        klass: self.class.name,
        errors: errors,
        line_number: @line_number,
        original_input_line: original_input_line
      } }.merge(
        @fields.keys.to_h do |key|
          [key, @fields[key].to_json_output]
        end)
  end

  # Returns a JSON string representing the record instance.
  # @param _args [Array] Arguments to be passed to `JSON.pretty_generate`.
  # @return [String] The JSON representation of the record's current state.
  def to_json(*_args)
    JSON.pretty_generate(to_h)
  end

  # Generates the 94-character ACH string representation of the record.
  #
  # This is done by concatenating the ACH string representation of each field.
  # @return [String] The 94-character ACH record string.
  def to_ach
    @fields.keys.collect do |key|
      @fields[key].to_ach
    end.join
  end

  # Generates an HTML representation of the record.
  #
  # This is useful for displaying ACH data in a web interface.
  # @param _opts [Hash] Options for HTML generation (currently unused).
  # @return [String] The HTML representation of the record.
  def to_html(_opts = {})
    record_error_class = nil

    field_html = @fields.values.collect do |field|
      record_error_class ||= 'error' if field.errors.any?
      field.to_html
    end.join
    "<div class=\"nacha-record tooltip #{record_type} #{record_error_class}\">" \
      "<span class=\"nacha-record-number\" data-name=\"record-number\">#{format('%05d',
        line_number)}&nbsp;|&nbsp</span>" \
      "#{field_html}" \
      "<span class=\"record-type\" data-name=\"record-type\">#{human_name}</span>" \
      "</div>"
  end

  # Returns a developer-friendly string representation of the record object.
  # @return [String] A string showing the class name and a hash representation of the object.
  def inspect
    "#<#{self.class.name}> #{to_h}"
  end

  # Returns the field definitions for the record class.
  # @return [Hash] A hash containing the definitions of all fields for this record type.
  # @see .definition
  def definition
    self.class.definition
  end

  # Runs all field-level and record-level validations.
  #
  # This method populates the `errors` array on the record and its fields.
  # @return [void]
  def validate
    # Run field-level validations first
    @fields.each_value(&:validate)
    # Then run record-level validations that might depend on multiple fields
    self.class.definition.each_key do |field|
      run_record_level_validations_for(field)
    end
  end

  # Checks if the record is valid by running all validations.
  # @return [Boolean] `true` if the record has no errors, `false` otherwise.
  def valid?
    validate
    errors.empty?
  end

  # Checks if the current transaction code represents a debit transaction.
  #
  # This method evaluates the `transaction_code` (which is expected to be
  # an attribute or method available in the current context) against a
  # predefined set of debit transaction codes.
  #
  # @return [Boolean] `true` if `transaction_code` is present and its
  #   string representation is included in `DEBIT_TRANSACTION_CODES`,
  #   `false` otherwise.
  #
  # @example
  #   # Assuming transaction_code is "201" and DEBIT_TRANSACTION_CODES includes "201"
  #   debit? #=> true
  #
  #   # Assuming transaction_code is "100" and DEBIT_TRANSACTION_CODES
  #   # does not include "100"
  #   debit? #=> false
  #
  #   # Assuming transaction_code is nil
  #   debit? #=> false
  #
  # @note
  #   This method includes robust error handling. If a `NoMethodError`
  #   occurs (e.g., if `transaction_code` is undefinable or does not respond
  #   to `to_s` in an unexpected way) or a `NameError` occurs (e.g., if
  #   `transaction_code` or `DEBIT_TRANSACTION_CODES` is not defined
  #   in the current scope), the method gracefully rescues these exceptions
  #   and returns `false`. This default behavior ensures that an inability
  #   to determine the transaction type results in it being considered
  #   "not a debit".
  #
  # @see #transaction_code (if `transaction_code` is an instance method or attribute)
  # @see DEBIT_TRANSACTION_CODES (the constant defining debit codes)
  def debit?
    transaction_code &&
      DEBIT_TRANSACTION_CODES.include?(transaction_code.to_s)
  rescue NoMethodError, NameError
    false
  end

  # Checks if the current transaction code represents a credit transaction.
  #
  # This method evaluates the `transaction_code` (which is expected to be
  # an attribute or method available in the current context) against a
  # predefined set of credit transaction codes.
  #
  # @return [Boolean] `true` if `transaction_code` is present and its
  #   string representation is included in `CREDIT_TRANSACTION_CODES`,
  #   `false` otherwise.
  #
  # @example
  #   # Assuming transaction_code is "101" and CREDIT_TRANSACTION_CODES includes "101"
  #   credit? #=> true
  #
  #   # Assuming transaction_code is "200" and CREDIT_TRANSACTION_CODES
  #   # does not include "200"
  #   credit? #=> false
  #
  #   # Assuming transaction_code is nil
  #   credit? #=> false
  #
  # @note
  #   This method includes robust error handling. If a `NoMethodError`
  #   occurs (e.g., if `transaction_code` is undefinable or does not respond
  #   to `to_s` in an unexpected way) or a `NameError` occurs (e.g., if
  #   `transaction_code` or `CREDIT_TRANSACTION_CODES` is not defined
  #   in the current scope), the method gracefully rescues these exceptions
  #   and returns `false`. This default behavior ensures that an inability
  #   to determine the transaction type results in it being considered
  #   "not a credit".
  #
  # @see #transaction_code (if `transaction_code` is an instance method or attribute)
  # @see CREDIT_TRANSACTION_CODES (the constant defining credit codes)
  def credit?
    transaction_code &&
      CREDIT_TRANSACTION_CODES.include?(transaction_code.to_s)
  rescue NoMethodError, NameError
    false
  end

  # Returns all validation errors for the record and its fields.
  # @return [Array<String>] An array of error messages.
  def errors
    (@errors + @fields.values.map(&:errors)).flatten
  end

  # Adds a validation error message to the record's error list.
  # @param err_string [String] The error message to add.
  # @return [void]
  def add_error(err_string)
    @errors << err_string
  end

  # :reek:TooManyStatements

  # Handles dynamic access to fields as methods.
  #
  # This allows getting and setting field data using attribute-style methods
  # (e.g., `record.amount`, `record.amount = 123.45`).
  # @param method_name [Symbol] The name of the missing method.
  # @param args [Array] The arguments passed to the method.
  # @param block [Proc] A block passed to the method.
  # @return [Nacha::Field, Object] Returns the `Nacha::Field` object for a getter, or the assigned value for a setter.
  def method_missing(method_name, *args, &block)
    method = method_name.to_s
    field_name = method.gsub(/=$/, '').to_sym
    field = @fields[field_name]
    return super unless field

    if method.end_with?('=')
      assign_field_data(field, args)
    else
      field
    end
  end

  # Complements `method_missing` to allow `respond_to?` to work for dynamic field methods.
  # @param method_name [Symbol] The name of the method to check.
  # @param _ [Array] Additional arguments (e.g. `include_private`) are ignored.
  # @return [Boolean] `true` if the method corresponds to a defined field, `false` otherwise.
  def respond_to_missing?(method_name, *)
    field_name = method_name.to_s.gsub(/=$/, '').to_sym
    definition[field_name] || super
  end

  private

  def assign_field_data(field, args)
    # rubocop:disable GitlabSecurity/PublicSend
    field.public_send(:data=, *args)
    # rubocop:enable GitlabSecurity/PublicSend
    @dirty = true
  end

  # :reek:TooManyStatements

  def run_record_level_validations_for(field)
    klass = self.class
    validations = klass.validations[field]
    return unless validations

    # rubocop:disable GitlabSecurity/PublicSend
    field_data = send(field)

    validations.each do |validation_method|
      klass.send(validation_method, field_data)
    end
    # rubocop:enable GitlabSecurity/PublicSend
  end
end

#nameObject (readonly)

Returns the value of attribute name.



24
25
26
# File 'lib/nacha/record/base.rb', line 24

def name
  @name
end

#original_input_lineString?

Returns The original string from the ACH file that this record was parsed from.

Returns:

  • (String, nil)

    The original string from the ACH file that this record was parsed from.



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
# File 'lib/nacha/record/base.rb', line 21

class Base
  include Validations::FieldValidations

  attr_reader :children, :name, :validations, :original_input_line, :fields

  # :reek:Attribute

  attr_accessor :parent, :line_number

  # :reek:ManualDispatch

  # Initializes a new record object.
  # @param opts [Hash] A hash of options to set as attributes on the new record.
  def initialize(opts = {})
    @children = []
    @parent = nil
    @errors = []
    @original_input_line = nil
    @line_number = nil
    @dirty = false
    @fields = {}
    create_fields_from_definition
    opts.each do |key, value|
      setter = "#{key}="
      # rubocop:disable GitlabSecurity/PublicSend
      public_send(setter, value) if value && respond_to?(setter)
      # rubocop:enable GitlabSecurity/PublicSend
    end
  end

  class << self
    # :reek:LongParameterList, :reek:ManualDispatch
    # Defines a field for the record type.
    #
    # This class method is used in subclasses to declare each field within the NACHA record.
    # It stores the definition and sets up validations.
    #
    # @param name [Symbol] The name of the field.
    # @param inclusion [String] The inclusion rule for the field ('M' for mandatory, 'R' for required, 'O' for optional).
    # @param contents [String] The expected content type of the field (e.g., 'Numeric', 'Alphameric', 'C...').
    # @param position [Range] The character position range of the field in the record string.
    # @return [void]
    def nacha_field(name, inclusion:, contents:, position:)
      Nacha.add_ach_record_type(self)
      definition[name] = { inclusion: inclusion,
                           contents: contents,
                           position: position,
                           name: name }
      validation_method = :"valid_#{name}"
      return unless respond_to?(validation_method)

      (validations[name] ||= []) << validation_method
    end

    # Returns the field definitions for the record class.
    # @return [Hash] A hash containing the definitions of all fields for this record type.
    def definition
      @definition ||= {}
    end

    # Returns the validation methods for the record class.
    # @return [Hash] A hash of validation methods for the fields of this record type.
    def validations
      @validations ||= {}
    end

    # Generates and returns a format string for `String#unpack` to parse a record line.
    #
    # The format string is built based on the field definitions.
    # @return [String] The unpack format string.
    def unpack_str
      @unpack_str ||= definition.values
                        .sort { |a, b| a[:position].first <=> b[:position].first }
                        .collect do |field_def|
        Nacha::Field.unpack_str(field_def)
      end.join.freeze
    end

    # :reek:TooManyStatements
    # rubocop:disable Layout/BlockAlignment,
    # rubocop:disable Style/StringConcatenation:

    # Generates a Regexp to match against a line to determine if it is of this record type.
    # @return [Regexp] The regular expression for matching record lines.
    def matcher
      @matcher ||= begin
        output_started = false
        skipped_output = false
        Regexp.new('\A' + definition.values
                            .sort { |a,b| a[:position].first <=> b[:position].first }.reverse.collect do |field|
                     contents = field[:contents]
                     position = field[:position]
                     size = position.size
                     if contents =~ /\AC(.+)\z/
                       last_match = Regexp.last_match(1)
                       if last_match.match?(/  */) && !output_started
                         skipped_output = true
                         ''
                       else
                         output_started = true
                         last_match
                       end
                     elsif contents.match?(/\ANumeric\z/) || contents.match?(/\AYYMMDD\z/)
                       output_started = true
                       "[0-9 ]{#{size}}"
                     elsif output_started
                       ".{#{size}}"
                     else
                       skipped_output = true
                       ''
                     end
                   end.reverse.join + (skipped_output ? '.*' : '') + '\z')
      end
    end
    # rubocop:enable Layout/BlockAlignment,
    # rubocop:enable Style/StringConcatenation:

    # Parses an ACH string and creates a new record instance.
    #
    # It unpacks the string based on field definitions, populates the fields,
    # and runs validations.
    # @param ach_str [String] The 94-character string representing a NACHA record.
    # @return [Nacha::Record::Base] A new instance of the record class populated with data from the string.
    def parse(ach_str)
      rec = new(original_input_line: ach_str)
      ach_str.unpack(unpack_str).zip(rec.fields.values) do |input_data, field|
        field.data = input_data
      end
      rec.validate
      rec
    end

    # Returns the record type name.
    #
    # The name is derived from the class name, converted to a snake_case symbol.
    # @return [Symbol] The record type as a symbol (e.g., :file_header).
    def record_type
      Nacha.record_name(self)
    end

    # Returns a hash representation of the record class definition.
    #
    # This includes field definitions and child record types.
    # @return [Hash] A hash representing the structure of the record type.
    def to_h # :reek:ManualDispatch, :reek:TooManyStatements
      fields = definition.transform_values do |field_def|
        {
          inclusion: field_def[:inclusion],
          contents: field_def[:contents],
          position: field_def[:position].to_s
        }
      end

      fields[:child_record_types] = child_record_types.to_a
      fields[:child_record_types] ||= []
      fields[:klass] = name.to_s

      { record_type.to_sym => fields }
    end

    # Returns a JSON string representing the record class definition.
    # @param _args [Array] Arguments to be passed to `JSON.pretty_generate`.
    # @return [String] The JSON representation of the record definition.
    def to_json(*_args)
      JSON.pretty_generate(to_h)
    end
  end

  # :reek:FeatureEnvy

  # Sets the original input line string for the record.
  # @param line [String] The original 94-character string from the ACH file.
  # @return [void]
  def original_input_line=(line)
    @original_input_line = line.dup if line.is_a?(String)
  end

  # Creates field instances from the class's field definitions.
  #
  # This method is called during initialization to populate the `@fields` hash.
  # @return [void]
  def create_fields_from_definition
    definition.each_pair do |field_name, field_def|
      @fields[field_name.to_sym] = Nacha::Field.new(field_def)
    end
  end

  # Returns the record type name for the instance.
  # @return [Symbol] The record type as a symbol.
  # @see .record_type
  def record_type
    self.class.record_type
  end

  # Returns a human-readable name for the record type.
  #
  # It converts the snake_case `record_type` into a capitalized string with spaces.
  # @return [String] The human-readable name (e.g., 'File Header').
  def human_name
    @human_name ||= record_type.to_s.split('_').map(&:capitalize).join(' ')
  end

  # Returns a hash representation of the record instance.
  #
  # The hash includes metadata and a representation of each field's data and errors.
  # @return [Hash] A hash representing the record's current state.
  def to_h
    { nacha_record_type: record_type,
      metadata: {
        klass: self.class.name,
        errors: errors,
        line_number: @line_number,
        original_input_line: original_input_line
      } }.merge(
        @fields.keys.to_h do |key|
          [key, @fields[key].to_json_output]
        end)
  end

  # Returns a JSON string representing the record instance.
  # @param _args [Array] Arguments to be passed to `JSON.pretty_generate`.
  # @return [String] The JSON representation of the record's current state.
  def to_json(*_args)
    JSON.pretty_generate(to_h)
  end

  # Generates the 94-character ACH string representation of the record.
  #
  # This is done by concatenating the ACH string representation of each field.
  # @return [String] The 94-character ACH record string.
  def to_ach
    @fields.keys.collect do |key|
      @fields[key].to_ach
    end.join
  end

  # Generates an HTML representation of the record.
  #
  # This is useful for displaying ACH data in a web interface.
  # @param _opts [Hash] Options for HTML generation (currently unused).
  # @return [String] The HTML representation of the record.
  def to_html(_opts = {})
    record_error_class = nil

    field_html = @fields.values.collect do |field|
      record_error_class ||= 'error' if field.errors.any?
      field.to_html
    end.join
    "<div class=\"nacha-record tooltip #{record_type} #{record_error_class}\">" \
      "<span class=\"nacha-record-number\" data-name=\"record-number\">#{format('%05d',
        line_number)}&nbsp;|&nbsp</span>" \
      "#{field_html}" \
      "<span class=\"record-type\" data-name=\"record-type\">#{human_name}</span>" \
      "</div>"
  end

  # Returns a developer-friendly string representation of the record object.
  # @return [String] A string showing the class name and a hash representation of the object.
  def inspect
    "#<#{self.class.name}> #{to_h}"
  end

  # Returns the field definitions for the record class.
  # @return [Hash] A hash containing the definitions of all fields for this record type.
  # @see .definition
  def definition
    self.class.definition
  end

  # Runs all field-level and record-level validations.
  #
  # This method populates the `errors` array on the record and its fields.
  # @return [void]
  def validate
    # Run field-level validations first
    @fields.each_value(&:validate)
    # Then run record-level validations that might depend on multiple fields
    self.class.definition.each_key do |field|
      run_record_level_validations_for(field)
    end
  end

  # Checks if the record is valid by running all validations.
  # @return [Boolean] `true` if the record has no errors, `false` otherwise.
  def valid?
    validate
    errors.empty?
  end

  # Checks if the current transaction code represents a debit transaction.
  #
  # This method evaluates the `transaction_code` (which is expected to be
  # an attribute or method available in the current context) against a
  # predefined set of debit transaction codes.
  #
  # @return [Boolean] `true` if `transaction_code` is present and its
  #   string representation is included in `DEBIT_TRANSACTION_CODES`,
  #   `false` otherwise.
  #
  # @example
  #   # Assuming transaction_code is "201" and DEBIT_TRANSACTION_CODES includes "201"
  #   debit? #=> true
  #
  #   # Assuming transaction_code is "100" and DEBIT_TRANSACTION_CODES
  #   # does not include "100"
  #   debit? #=> false
  #
  #   # Assuming transaction_code is nil
  #   debit? #=> false
  #
  # @note
  #   This method includes robust error handling. If a `NoMethodError`
  #   occurs (e.g., if `transaction_code` is undefinable or does not respond
  #   to `to_s` in an unexpected way) or a `NameError` occurs (e.g., if
  #   `transaction_code` or `DEBIT_TRANSACTION_CODES` is not defined
  #   in the current scope), the method gracefully rescues these exceptions
  #   and returns `false`. This default behavior ensures that an inability
  #   to determine the transaction type results in it being considered
  #   "not a debit".
  #
  # @see #transaction_code (if `transaction_code` is an instance method or attribute)
  # @see DEBIT_TRANSACTION_CODES (the constant defining debit codes)
  def debit?
    transaction_code &&
      DEBIT_TRANSACTION_CODES.include?(transaction_code.to_s)
  rescue NoMethodError, NameError
    false
  end

  # Checks if the current transaction code represents a credit transaction.
  #
  # This method evaluates the `transaction_code` (which is expected to be
  # an attribute or method available in the current context) against a
  # predefined set of credit transaction codes.
  #
  # @return [Boolean] `true` if `transaction_code` is present and its
  #   string representation is included in `CREDIT_TRANSACTION_CODES`,
  #   `false` otherwise.
  #
  # @example
  #   # Assuming transaction_code is "101" and CREDIT_TRANSACTION_CODES includes "101"
  #   credit? #=> true
  #
  #   # Assuming transaction_code is "200" and CREDIT_TRANSACTION_CODES
  #   # does not include "200"
  #   credit? #=> false
  #
  #   # Assuming transaction_code is nil
  #   credit? #=> false
  #
  # @note
  #   This method includes robust error handling. If a `NoMethodError`
  #   occurs (e.g., if `transaction_code` is undefinable or does not respond
  #   to `to_s` in an unexpected way) or a `NameError` occurs (e.g., if
  #   `transaction_code` or `CREDIT_TRANSACTION_CODES` is not defined
  #   in the current scope), the method gracefully rescues these exceptions
  #   and returns `false`. This default behavior ensures that an inability
  #   to determine the transaction type results in it being considered
  #   "not a credit".
  #
  # @see #transaction_code (if `transaction_code` is an instance method or attribute)
  # @see CREDIT_TRANSACTION_CODES (the constant defining credit codes)
  def credit?
    transaction_code &&
      CREDIT_TRANSACTION_CODES.include?(transaction_code.to_s)
  rescue NoMethodError, NameError
    false
  end

  # Returns all validation errors for the record and its fields.
  # @return [Array<String>] An array of error messages.
  def errors
    (@errors + @fields.values.map(&:errors)).flatten
  end

  # Adds a validation error message to the record's error list.
  # @param err_string [String] The error message to add.
  # @return [void]
  def add_error(err_string)
    @errors << err_string
  end

  # :reek:TooManyStatements

  # Handles dynamic access to fields as methods.
  #
  # This allows getting and setting field data using attribute-style methods
  # (e.g., `record.amount`, `record.amount = 123.45`).
  # @param method_name [Symbol] The name of the missing method.
  # @param args [Array] The arguments passed to the method.
  # @param block [Proc] A block passed to the method.
  # @return [Nacha::Field, Object] Returns the `Nacha::Field` object for a getter, or the assigned value for a setter.
  def method_missing(method_name, *args, &block)
    method = method_name.to_s
    field_name = method.gsub(/=$/, '').to_sym
    field = @fields[field_name]
    return super unless field

    if method.end_with?('=')
      assign_field_data(field, args)
    else
      field
    end
  end

  # Complements `method_missing` to allow `respond_to?` to work for dynamic field methods.
  # @param method_name [Symbol] The name of the method to check.
  # @param _ [Array] Additional arguments (e.g. `include_private`) are ignored.
  # @return [Boolean] `true` if the method corresponds to a defined field, `false` otherwise.
  def respond_to_missing?(method_name, *)
    field_name = method_name.to_s.gsub(/=$/, '').to_sym
    definition[field_name] || super
  end

  private

  def assign_field_data(field, args)
    # rubocop:disable GitlabSecurity/PublicSend
    field.public_send(:data=, *args)
    # rubocop:enable GitlabSecurity/PublicSend
    @dirty = true
  end

  # :reek:TooManyStatements

  def run_record_level_validations_for(field)
    klass = self.class
    validations = klass.validations[field]
    return unless validations

    # rubocop:disable GitlabSecurity/PublicSend
    field_data = send(field)

    validations.each do |validation_method|
      klass.send(validation_method, field_data)
    end
    # rubocop:enable GitlabSecurity/PublicSend
  end
end

#parentObject

:reek:Attribute



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
# File 'lib/nacha/record/base.rb', line 21

class Base
  include Validations::FieldValidations

  attr_reader :children, :name, :validations, :original_input_line, :fields

  # :reek:Attribute

  attr_accessor :parent, :line_number

  # :reek:ManualDispatch

  # Initializes a new record object.
  # @param opts [Hash] A hash of options to set as attributes on the new record.
  def initialize(opts = {})
    @children = []
    @parent = nil
    @errors = []
    @original_input_line = nil
    @line_number = nil
    @dirty = false
    @fields = {}
    create_fields_from_definition
    opts.each do |key, value|
      setter = "#{key}="
      # rubocop:disable GitlabSecurity/PublicSend
      public_send(setter, value) if value && respond_to?(setter)
      # rubocop:enable GitlabSecurity/PublicSend
    end
  end

  class << self
    # :reek:LongParameterList, :reek:ManualDispatch
    # Defines a field for the record type.
    #
    # This class method is used in subclasses to declare each field within the NACHA record.
    # It stores the definition and sets up validations.
    #
    # @param name [Symbol] The name of the field.
    # @param inclusion [String] The inclusion rule for the field ('M' for mandatory, 'R' for required, 'O' for optional).
    # @param contents [String] The expected content type of the field (e.g., 'Numeric', 'Alphameric', 'C...').
    # @param position [Range] The character position range of the field in the record string.
    # @return [void]
    def nacha_field(name, inclusion:, contents:, position:)
      Nacha.add_ach_record_type(self)
      definition[name] = { inclusion: inclusion,
                           contents: contents,
                           position: position,
                           name: name }
      validation_method = :"valid_#{name}"
      return unless respond_to?(validation_method)

      (validations[name] ||= []) << validation_method
    end

    # Returns the field definitions for the record class.
    # @return [Hash] A hash containing the definitions of all fields for this record type.
    def definition
      @definition ||= {}
    end

    # Returns the validation methods for the record class.
    # @return [Hash] A hash of validation methods for the fields of this record type.
    def validations
      @validations ||= {}
    end

    # Generates and returns a format string for `String#unpack` to parse a record line.
    #
    # The format string is built based on the field definitions.
    # @return [String] The unpack format string.
    def unpack_str
      @unpack_str ||= definition.values
                        .sort { |a, b| a[:position].first <=> b[:position].first }
                        .collect do |field_def|
        Nacha::Field.unpack_str(field_def)
      end.join.freeze
    end

    # :reek:TooManyStatements
    # rubocop:disable Layout/BlockAlignment,
    # rubocop:disable Style/StringConcatenation:

    # Generates a Regexp to match against a line to determine if it is of this record type.
    # @return [Regexp] The regular expression for matching record lines.
    def matcher
      @matcher ||= begin
        output_started = false
        skipped_output = false
        Regexp.new('\A' + definition.values
                            .sort { |a,b| a[:position].first <=> b[:position].first }.reverse.collect do |field|
                     contents = field[:contents]
                     position = field[:position]
                     size = position.size
                     if contents =~ /\AC(.+)\z/
                       last_match = Regexp.last_match(1)
                       if last_match.match?(/  */) && !output_started
                         skipped_output = true
                         ''
                       else
                         output_started = true
                         last_match
                       end
                     elsif contents.match?(/\ANumeric\z/) || contents.match?(/\AYYMMDD\z/)
                       output_started = true
                       "[0-9 ]{#{size}}"
                     elsif output_started
                       ".{#{size}}"
                     else
                       skipped_output = true
                       ''
                     end
                   end.reverse.join + (skipped_output ? '.*' : '') + '\z')
      end
    end
    # rubocop:enable Layout/BlockAlignment,
    # rubocop:enable Style/StringConcatenation:

    # Parses an ACH string and creates a new record instance.
    #
    # It unpacks the string based on field definitions, populates the fields,
    # and runs validations.
    # @param ach_str [String] The 94-character string representing a NACHA record.
    # @return [Nacha::Record::Base] A new instance of the record class populated with data from the string.
    def parse(ach_str)
      rec = new(original_input_line: ach_str)
      ach_str.unpack(unpack_str).zip(rec.fields.values) do |input_data, field|
        field.data = input_data
      end
      rec.validate
      rec
    end

    # Returns the record type name.
    #
    # The name is derived from the class name, converted to a snake_case symbol.
    # @return [Symbol] The record type as a symbol (e.g., :file_header).
    def record_type
      Nacha.record_name(self)
    end

    # Returns a hash representation of the record class definition.
    #
    # This includes field definitions and child record types.
    # @return [Hash] A hash representing the structure of the record type.
    def to_h # :reek:ManualDispatch, :reek:TooManyStatements
      fields = definition.transform_values do |field_def|
        {
          inclusion: field_def[:inclusion],
          contents: field_def[:contents],
          position: field_def[:position].to_s
        }
      end

      fields[:child_record_types] = child_record_types.to_a
      fields[:child_record_types] ||= []
      fields[:klass] = name.to_s

      { record_type.to_sym => fields }
    end

    # Returns a JSON string representing the record class definition.
    # @param _args [Array] Arguments to be passed to `JSON.pretty_generate`.
    # @return [String] The JSON representation of the record definition.
    def to_json(*_args)
      JSON.pretty_generate(to_h)
    end
  end

  # :reek:FeatureEnvy

  # Sets the original input line string for the record.
  # @param line [String] The original 94-character string from the ACH file.
  # @return [void]
  def original_input_line=(line)
    @original_input_line = line.dup if line.is_a?(String)
  end

  # Creates field instances from the class's field definitions.
  #
  # This method is called during initialization to populate the `@fields` hash.
  # @return [void]
  def create_fields_from_definition
    definition.each_pair do |field_name, field_def|
      @fields[field_name.to_sym] = Nacha::Field.new(field_def)
    end
  end

  # Returns the record type name for the instance.
  # @return [Symbol] The record type as a symbol.
  # @see .record_type
  def record_type
    self.class.record_type
  end

  # Returns a human-readable name for the record type.
  #
  # It converts the snake_case `record_type` into a capitalized string with spaces.
  # @return [String] The human-readable name (e.g., 'File Header').
  def human_name
    @human_name ||= record_type.to_s.split('_').map(&:capitalize).join(' ')
  end

  # Returns a hash representation of the record instance.
  #
  # The hash includes metadata and a representation of each field's data and errors.
  # @return [Hash] A hash representing the record's current state.
  def to_h
    { nacha_record_type: record_type,
      metadata: {
        klass: self.class.name,
        errors: errors,
        line_number: @line_number,
        original_input_line: original_input_line
      } }.merge(
        @fields.keys.to_h do |key|
          [key, @fields[key].to_json_output]
        end)
  end

  # Returns a JSON string representing the record instance.
  # @param _args [Array] Arguments to be passed to `JSON.pretty_generate`.
  # @return [String] The JSON representation of the record's current state.
  def to_json(*_args)
    JSON.pretty_generate(to_h)
  end

  # Generates the 94-character ACH string representation of the record.
  #
  # This is done by concatenating the ACH string representation of each field.
  # @return [String] The 94-character ACH record string.
  def to_ach
    @fields.keys.collect do |key|
      @fields[key].to_ach
    end.join
  end

  # Generates an HTML representation of the record.
  #
  # This is useful for displaying ACH data in a web interface.
  # @param _opts [Hash] Options for HTML generation (currently unused).
  # @return [String] The HTML representation of the record.
  def to_html(_opts = {})
    record_error_class = nil

    field_html = @fields.values.collect do |field|
      record_error_class ||= 'error' if field.errors.any?
      field.to_html
    end.join
    "<div class=\"nacha-record tooltip #{record_type} #{record_error_class}\">" \
      "<span class=\"nacha-record-number\" data-name=\"record-number\">#{format('%05d',
        line_number)}&nbsp;|&nbsp</span>" \
      "#{field_html}" \
      "<span class=\"record-type\" data-name=\"record-type\">#{human_name}</span>" \
      "</div>"
  end

  # Returns a developer-friendly string representation of the record object.
  # @return [String] A string showing the class name and a hash representation of the object.
  def inspect
    "#<#{self.class.name}> #{to_h}"
  end

  # Returns the field definitions for the record class.
  # @return [Hash] A hash containing the definitions of all fields for this record type.
  # @see .definition
  def definition
    self.class.definition
  end

  # Runs all field-level and record-level validations.
  #
  # This method populates the `errors` array on the record and its fields.
  # @return [void]
  def validate
    # Run field-level validations first
    @fields.each_value(&:validate)
    # Then run record-level validations that might depend on multiple fields
    self.class.definition.each_key do |field|
      run_record_level_validations_for(field)
    end
  end

  # Checks if the record is valid by running all validations.
  # @return [Boolean] `true` if the record has no errors, `false` otherwise.
  def valid?
    validate
    errors.empty?
  end

  # Checks if the current transaction code represents a debit transaction.
  #
  # This method evaluates the `transaction_code` (which is expected to be
  # an attribute or method available in the current context) against a
  # predefined set of debit transaction codes.
  #
  # @return [Boolean] `true` if `transaction_code` is present and its
  #   string representation is included in `DEBIT_TRANSACTION_CODES`,
  #   `false` otherwise.
  #
  # @example
  #   # Assuming transaction_code is "201" and DEBIT_TRANSACTION_CODES includes "201"
  #   debit? #=> true
  #
  #   # Assuming transaction_code is "100" and DEBIT_TRANSACTION_CODES
  #   # does not include "100"
  #   debit? #=> false
  #
  #   # Assuming transaction_code is nil
  #   debit? #=> false
  #
  # @note
  #   This method includes robust error handling. If a `NoMethodError`
  #   occurs (e.g., if `transaction_code` is undefinable or does not respond
  #   to `to_s` in an unexpected way) or a `NameError` occurs (e.g., if
  #   `transaction_code` or `DEBIT_TRANSACTION_CODES` is not defined
  #   in the current scope), the method gracefully rescues these exceptions
  #   and returns `false`. This default behavior ensures that an inability
  #   to determine the transaction type results in it being considered
  #   "not a debit".
  #
  # @see #transaction_code (if `transaction_code` is an instance method or attribute)
  # @see DEBIT_TRANSACTION_CODES (the constant defining debit codes)
  def debit?
    transaction_code &&
      DEBIT_TRANSACTION_CODES.include?(transaction_code.to_s)
  rescue NoMethodError, NameError
    false
  end

  # Checks if the current transaction code represents a credit transaction.
  #
  # This method evaluates the `transaction_code` (which is expected to be
  # an attribute or method available in the current context) against a
  # predefined set of credit transaction codes.
  #
  # @return [Boolean] `true` if `transaction_code` is present and its
  #   string representation is included in `CREDIT_TRANSACTION_CODES`,
  #   `false` otherwise.
  #
  # @example
  #   # Assuming transaction_code is "101" and CREDIT_TRANSACTION_CODES includes "101"
  #   credit? #=> true
  #
  #   # Assuming transaction_code is "200" and CREDIT_TRANSACTION_CODES
  #   # does not include "200"
  #   credit? #=> false
  #
  #   # Assuming transaction_code is nil
  #   credit? #=> false
  #
  # @note
  #   This method includes robust error handling. If a `NoMethodError`
  #   occurs (e.g., if `transaction_code` is undefinable or does not respond
  #   to `to_s` in an unexpected way) or a `NameError` occurs (e.g., if
  #   `transaction_code` or `CREDIT_TRANSACTION_CODES` is not defined
  #   in the current scope), the method gracefully rescues these exceptions
  #   and returns `false`. This default behavior ensures that an inability
  #   to determine the transaction type results in it being considered
  #   "not a credit".
  #
  # @see #transaction_code (if `transaction_code` is an instance method or attribute)
  # @see CREDIT_TRANSACTION_CODES (the constant defining credit codes)
  def credit?
    transaction_code &&
      CREDIT_TRANSACTION_CODES.include?(transaction_code.to_s)
  rescue NoMethodError, NameError
    false
  end

  # Returns all validation errors for the record and its fields.
  # @return [Array<String>] An array of error messages.
  def errors
    (@errors + @fields.values.map(&:errors)).flatten
  end

  # Adds a validation error message to the record's error list.
  # @param err_string [String] The error message to add.
  # @return [void]
  def add_error(err_string)
    @errors << err_string
  end

  # :reek:TooManyStatements

  # Handles dynamic access to fields as methods.
  #
  # This allows getting and setting field data using attribute-style methods
  # (e.g., `record.amount`, `record.amount = 123.45`).
  # @param method_name [Symbol] The name of the missing method.
  # @param args [Array] The arguments passed to the method.
  # @param block [Proc] A block passed to the method.
  # @return [Nacha::Field, Object] Returns the `Nacha::Field` object for a getter, or the assigned value for a setter.
  def method_missing(method_name, *args, &block)
    method = method_name.to_s
    field_name = method.gsub(/=$/, '').to_sym
    field = @fields[field_name]
    return super unless field

    if method.end_with?('=')
      assign_field_data(field, args)
    else
      field
    end
  end

  # Complements `method_missing` to allow `respond_to?` to work for dynamic field methods.
  # @param method_name [Symbol] The name of the method to check.
  # @param _ [Array] Additional arguments (e.g. `include_private`) are ignored.
  # @return [Boolean] `true` if the method corresponds to a defined field, `false` otherwise.
  def respond_to_missing?(method_name, *)
    field_name = method_name.to_s.gsub(/=$/, '').to_sym
    definition[field_name] || super
  end

  private

  def assign_field_data(field, args)
    # rubocop:disable GitlabSecurity/PublicSend
    field.public_send(:data=, *args)
    # rubocop:enable GitlabSecurity/PublicSend
    @dirty = true
  end

  # :reek:TooManyStatements

  def run_record_level_validations_for(field)
    klass = self.class
    validations = klass.validations[field]
    return unless validations

    # rubocop:disable GitlabSecurity/PublicSend
    field_data = send(field)

    validations.each do |validation_method|
      klass.send(validation_method, field_data)
    end
    # rubocop:enable GitlabSecurity/PublicSend
  end
end

#validationsObject (readonly)

Returns the value of attribute validations.



24
25
26
# File 'lib/nacha/record/base.rb', line 24

def validations
  @validations
end

Class Method Details

.definitionHash

Returns the field definitions for the record class.

Returns:

  • (Hash)

    A hash containing the definitions of all fields for this record type.



77
78
79
# File 'lib/nacha/record/base.rb', line 77

def definition
  @definition ||= {}
end

.matcherRegexp

Generates a Regexp to match against a line to determine if it is of this record type.

Returns:

  • (Regexp)

    The regular expression for matching record lines.



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/nacha/record/base.rb', line 105

def matcher
  @matcher ||= begin
    output_started = false
    skipped_output = false
    Regexp.new('\A' + definition.values
                        .sort { |a,b| a[:position].first <=> b[:position].first }.reverse.collect do |field|
                 contents = field[:contents]
                 position = field[:position]
                 size = position.size
                 if contents =~ /\AC(.+)\z/
                   last_match = Regexp.last_match(1)
                   if last_match.match?(/  */) && !output_started
                     skipped_output = true
                     ''
                   else
                     output_started = true
                     last_match
                   end
                 elsif contents.match?(/\ANumeric\z/) || contents.match?(/\AYYMMDD\z/)
                   output_started = true
                   "[0-9 ]{#{size}}"
                 elsif output_started
                   ".{#{size}}"
                 else
                   skipped_output = true
                   ''
                 end
               end.reverse.join + (skipped_output ? '.*' : '') + '\z')
  end
end

.nacha_field(name, inclusion:, contents:, position:) ⇒ void

This method returns an undefined value.

:reek:LongParameterList, :reek:ManualDispatch Defines a field for the record type.

This class method is used in subclasses to declare each field within the NACHA record. It stores the definition and sets up validations.

Parameters:

  • name (Symbol)

    The name of the field.

  • inclusion (String)

    The inclusion rule for the field (‘M’ for mandatory, ‘R’ for required, ‘O’ for optional).

  • contents (String)

    The expected content type of the field (e.g., ‘Numeric’, ‘Alphameric’, ‘C…’).

  • position (Range)

    The character position range of the field in the record string.



63
64
65
66
67
68
69
70
71
72
73
# File 'lib/nacha/record/base.rb', line 63

def nacha_field(name, inclusion:, contents:, position:)
  Nacha.add_ach_record_type(self)
  definition[name] = { inclusion: inclusion,
                       contents: contents,
                       position: position,
                       name: name }
  validation_method = :"valid_#{name}"
  return unless respond_to?(validation_method)

  (validations[name] ||= []) << validation_method
end

.parse(ach_str) ⇒ Nacha::Record::Base

Parses an ACH string and creates a new record instance.

It unpacks the string based on field definitions, populates the fields, and runs validations.

Parameters:

  • ach_str (String)

    The 94-character string representing a NACHA record.

Returns:

  • (Nacha::Record::Base)

    A new instance of the record class populated with data from the string.



144
145
146
147
148
149
150
151
# File 'lib/nacha/record/base.rb', line 144

def parse(ach_str)
  rec = new(original_input_line: ach_str)
  ach_str.unpack(unpack_str).zip(rec.fields.values) do |input_data, field|
    field.data = input_data
  end
  rec.validate
  rec
end

.record_typeSymbol

Returns the record type name.

The name is derived from the class name, converted to a snake_case symbol.

Returns:

  • (Symbol)

    The record type as a symbol (e.g., :file_header).



157
158
159
# File 'lib/nacha/record/base.rb', line 157

def record_type
  Nacha.record_name(self)
end

.to_hHash

Returns a hash representation of the record class definition.

This includes field definitions and child record types.

Returns:

  • (Hash)

    A hash representing the structure of the record type.



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/nacha/record/base.rb', line 165

def to_h # :reek:ManualDispatch, :reek:TooManyStatements
  fields = definition.transform_values do |field_def|
    {
      inclusion: field_def[:inclusion],
      contents: field_def[:contents],
      position: field_def[:position].to_s
    }
  end

  fields[:child_record_types] = child_record_types.to_a
  fields[:child_record_types] ||= []
  fields[:klass] = name.to_s

  { record_type.to_sym => fields }
end

.to_json(*_args) ⇒ String

Returns a JSON string representing the record class definition.

Parameters:

  • _args (Array)

    Arguments to be passed to ‘JSON.pretty_generate`.

Returns:

  • (String)

    The JSON representation of the record definition.



184
185
186
# File 'lib/nacha/record/base.rb', line 184

def to_json(*_args)
  JSON.pretty_generate(to_h)
end

.unpack_strString

Generates and returns a format string for ‘String#unpack` to parse a record line.

The format string is built based on the field definitions.

Returns:

  • (String)

    The unpack format string.



91
92
93
94
95
96
97
# File 'lib/nacha/record/base.rb', line 91

def unpack_str
  @unpack_str ||= definition.values
                    .sort { |a, b| a[:position].first <=> b[:position].first }
                    .collect do |field_def|
    Nacha::Field.unpack_str(field_def)
  end.join.freeze
end

.validationsHash

Returns the validation methods for the record class.

Returns:

  • (Hash)

    A hash of validation methods for the fields of this record type.



83
84
85
# File 'lib/nacha/record/base.rb', line 83

def validations
  @validations ||= {}
end

Instance Method Details

#add_error(err_string) ⇒ void

This method returns an undefined value.

Adds a validation error message to the record’s error list.

Parameters:

  • err_string (String)

    The error message to add.



399
400
401
# File 'lib/nacha/record/base.rb', line 399

def add_error(err_string)
  @errors << err_string
end

#create_fields_from_definitionvoid

This method returns an undefined value.

Creates field instances from the class’s field definitions.

This method is called during initialization to populate the ‘@fields` hash.



202
203
204
205
206
# File 'lib/nacha/record/base.rb', line 202

def create_fields_from_definition
  definition.each_pair do |field_name, field_def|
    @fields[field_name.to_sym] = Nacha::Field.new(field_def)
  end
end

#credit?Boolean

Note:

This method includes robust error handling. If a ‘NoMethodError` occurs (e.g., if `transaction_code` is undefinable or does not respond to `to_s` in an unexpected way) or a `NameError` occurs (e.g., if `transaction_code` or `CREDIT_TRANSACTION_CODES` is not defined in the current scope), the method gracefully rescues these exceptions and returns `false`. This default behavior ensures that an inability to determine the transaction type results in it being considered “not a credit”.

Checks if the current transaction code represents a credit transaction.

This method evaluates the ‘transaction_code` (which is expected to be an attribute or method available in the current context) against a predefined set of credit transaction codes.

Examples:

# Assuming transaction_code is "101" and CREDIT_TRANSACTION_CODES includes "101"
credit? #=> true

# Assuming transaction_code is "200" and CREDIT_TRANSACTION_CODES
# does not include "200"
credit? #=> false

# Assuming transaction_code is nil
credit? #=> false

Returns:

  • (Boolean)

    ‘true` if `transaction_code` is present and its string representation is included in `CREDIT_TRANSACTION_CODES`, `false` otherwise.

See Also:



383
384
385
386
387
388
# File 'lib/nacha/record/base.rb', line 383

def credit?
  transaction_code &&
    CREDIT_TRANSACTION_CODES.include?(transaction_code.to_s)
rescue NoMethodError, NameError
  false
end

#debit?Boolean

Note:

This method includes robust error handling. If a ‘NoMethodError` occurs (e.g., if `transaction_code` is undefinable or does not respond to `to_s` in an unexpected way) or a `NameError` occurs (e.g., if `transaction_code` or `DEBIT_TRANSACTION_CODES` is not defined in the current scope), the method gracefully rescues these exceptions and returns `false`. This default behavior ensures that an inability to determine the transaction type results in it being considered “not a debit”.

Checks if the current transaction code represents a debit transaction.

This method evaluates the ‘transaction_code` (which is expected to be an attribute or method available in the current context) against a predefined set of debit transaction codes.

Examples:

# Assuming transaction_code is "201" and DEBIT_TRANSACTION_CODES includes "201"
debit? #=> true

# Assuming transaction_code is "100" and DEBIT_TRANSACTION_CODES
# does not include "100"
debit? #=> false

# Assuming transaction_code is nil
debit? #=> false

Returns:

  • (Boolean)

    ‘true` if `transaction_code` is present and its string representation is included in `DEBIT_TRANSACTION_CODES`, `false` otherwise.

See Also:



343
344
345
346
347
348
# File 'lib/nacha/record/base.rb', line 343

def debit?
  transaction_code &&
    DEBIT_TRANSACTION_CODES.include?(transaction_code.to_s)
rescue NoMethodError, NameError
  false
end

#definitionHash

Returns the field definitions for the record class.

Returns:

  • (Hash)

    A hash containing the definitions of all fields for this record type.

See Also:



286
287
288
# File 'lib/nacha/record/base.rb', line 286

def definition
  self.class.definition
end

#errorsArray<String>

Returns all validation errors for the record and its fields.

Returns:

  • (Array<String>)

    An array of error messages.



392
393
394
# File 'lib/nacha/record/base.rb', line 392

def errors
  (@errors + @fields.values.map(&:errors)).flatten
end

#human_nameString

Returns a human-readable name for the record type.

It converts the snake_case ‘record_type` into a capitalized string with spaces.

Returns:

  • (String)

    The human-readable name (e.g., ‘File Header’).



219
220
221
# File 'lib/nacha/record/base.rb', line 219

def human_name
  @human_name ||= record_type.to_s.split('_').map(&:capitalize).join(' ')
end

#inspectString

Returns a developer-friendly string representation of the record object.

Returns:

  • (String)

    A string showing the class name and a hash representation of the object.



279
280
281
# File 'lib/nacha/record/base.rb', line 279

def inspect
  "#<#{self.class.name}> #{to_h}"
end

#record_typeSymbol

Returns the record type name for the instance.

Returns:

  • (Symbol)

    The record type as a symbol.

See Also:



211
212
213
# File 'lib/nacha/record/base.rb', line 211

def record_type
  self.class.record_type
end

#respond_to_missing?(method_name) ⇒ Boolean

Complements ‘method_missing` to allow `respond_to?` to work for dynamic field methods.

Parameters:

  • method_name (Symbol)

    The name of the method to check.

  • _ (Array)

    Additional arguments (e.g. ‘include_private`) are ignored.

Returns:

  • (Boolean)

    ‘true` if the method corresponds to a defined field, `false` otherwise.



430
431
432
433
# File 'lib/nacha/record/base.rb', line 430

def respond_to_missing?(method_name, *)
  field_name = method_name.to_s.gsub(/=$/, '').to_sym
  definition[field_name] || super
end

#to_achString

Generates the 94-character ACH string representation of the record.

This is done by concatenating the ACH string representation of each field.

Returns:

  • (String)

    The 94-character ACH record string.



251
252
253
254
255
# File 'lib/nacha/record/base.rb', line 251

def to_ach
  @fields.keys.collect do |key|
    @fields[key].to_ach
  end.join
end

#to_hHash

Returns a hash representation of the record instance.

The hash includes metadata and a representation of each field’s data and errors.

Returns:

  • (Hash)

    A hash representing the record’s current state.



227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/nacha/record/base.rb', line 227

def to_h
  { nacha_record_type: record_type,
    metadata: {
      klass: self.class.name,
      errors: errors,
      line_number: @line_number,
      original_input_line: original_input_line
    } }.merge(
      @fields.keys.to_h do |key|
        [key, @fields[key].to_json_output]
      end)
end

#to_html(_opts = {}) ⇒ String

Generates an HTML representation of the record.

This is useful for displaying ACH data in a web interface.

Parameters:

  • _opts (Hash) (defaults to: {})

    Options for HTML generation (currently unused).

Returns:

  • (String)

    The HTML representation of the record.



262
263
264
265
266
267
268
269
270
271
272
273
274
275
# File 'lib/nacha/record/base.rb', line 262

def to_html(_opts = {})
  record_error_class = nil

  field_html = @fields.values.collect do |field|
    record_error_class ||= 'error' if field.errors.any?
    field.to_html
  end.join
  "<div class=\"nacha-record tooltip #{record_type} #{record_error_class}\">" \
    "<span class=\"nacha-record-number\" data-name=\"record-number\">#{format('%05d',
      line_number)}&nbsp;|&nbsp</span>" \
    "#{field_html}" \
    "<span class=\"record-type\" data-name=\"record-type\">#{human_name}</span>" \
    "</div>"
end

#to_json(*_args) ⇒ String

Returns a JSON string representing the record instance.

Parameters:

  • _args (Array)

    Arguments to be passed to ‘JSON.pretty_generate`.

Returns:

  • (String)

    The JSON representation of the record’s current state.



243
244
245
# File 'lib/nacha/record/base.rb', line 243

def to_json(*_args)
  JSON.pretty_generate(to_h)
end

#valid?Boolean

Checks if the record is valid by running all validations.

Returns:

  • (Boolean)

    ‘true` if the record has no errors, `false` otherwise.



305
306
307
308
# File 'lib/nacha/record/base.rb', line 305

def valid?
  validate
  errors.empty?
end

#validatevoid

This method returns an undefined value.

Runs all field-level and record-level validations.

This method populates the ‘errors` array on the record and its fields.



294
295
296
297
298
299
300
301
# File 'lib/nacha/record/base.rb', line 294

def validate
  # Run field-level validations first
  @fields.each_value(&:validate)
  # Then run record-level validations that might depend on multiple fields
  self.class.definition.each_key do |field|
    run_record_level_validations_for(field)
  end
end