Class: Nacha::Record::Base
- Inherits:
-
Object
- Object
- Nacha::Record::Base
- Includes:
- Validations::FieldValidations
- Defined in:
- lib/nacha/record/base.rb
Overview
Base class for all Nacha records.
Direct Known Subclasses
AckEntryDetail, AdvBatchControl, AdvEntryDetail, AdvFileControl, AdvFileHeader, ArcEntryDetail, BatchControl, BatchHeader, BocEntryDetail, CcdAddenda, CcdEntryDetail, CieAddenda, CieEntryDetail, CtxAddenda, CtxCorporateEntryDetail, DneAddenda, DneEntryDetail, EnrAddenda, EnrEntryDetail, FifthIatAddenda, FileControl, FileHeader, Filler, FirstIatAddenda, FourthIatAddenda, IatBatchHeader, IatEntryDetail, IatForeignCoorespondentBankInformationAddenda, IatRemittanceInformationAddenda, MteAddenda, MteEntryDetail, PopEntryDetail, PosAddenda, PosEntryDetail, PpdAddenda, PpdEntryDetail, RckEntryDetail, SecondIatAddenda, SeventhIatAddenda, ShrAddenda, ShrEntryDetail, SixthIatAddenda, TelEntryDetail, ThirdIatAddenda, TrcEntryDetail, TrxAddenda, TrxEntryDetail, WebAddenda, WebEntryDetail, XckEntryDetail
Instance Attribute Summary collapse
-
#children ⇒ Array<Nacha::Record::Base>
readonly
An array of child records associated with this record.
-
#fields ⇒ Hash{Symbol => Nacha::Field}
readonly
A hash of field objects that belong to this record, keyed by field name.
-
#line_number ⇒ Object
:reek:Attribute.
-
#name ⇒ Object
readonly
Returns the value of attribute name.
-
#original_input_line ⇒ String?
The original string from the ACH file that this record was parsed from.
-
#parent ⇒ Object
:reek:Attribute.
-
#validations ⇒ Object
readonly
Returns the value of attribute validations.
Class Method Summary collapse
-
.definition ⇒ Hash
Returns the field definitions for the record class.
-
.matcher ⇒ Regexp
Generates a Regexp to match against a line to determine if it is of this record type.
-
.nacha_field(name, inclusion:, contents:, position:) ⇒ void
:reek:LongParameterList, :reek:ManualDispatch Defines a field for the record type.
-
.parse(ach_str) ⇒ Nacha::Record::Base
Parses an ACH string and creates a new record instance.
-
.record_type ⇒ Symbol
Returns the record type name.
-
.to_h ⇒ Hash
Returns a hash representation of the record class definition.
-
.to_json(*_args) ⇒ String
Returns a JSON string representing the record class definition.
-
.unpack_str ⇒ String
Generates and returns a format string for ‘String#unpack` to parse a record line.
-
.validations ⇒ Hash
Returns the validation methods for the record class.
Instance Method Summary collapse
-
#add_error(err_string) ⇒ void
Adds a validation error message to the record’s error list.
-
#create_fields_from_definition ⇒ void
Creates field instances from the class’s field definitions.
-
#credit? ⇒ Boolean
Checks if the current transaction code represents a credit transaction.
-
#debit? ⇒ Boolean
Checks if the current transaction code represents a debit transaction.
-
#definition ⇒ Hash
Returns the field definitions for the record class.
-
#errors ⇒ Array<String>
Returns all validation errors for the record and its fields.
-
#human_name ⇒ String
Returns a human-readable name for the record type.
-
#initialize(opts = {}) ⇒ Base
constructor
Initializes a new record object.
-
#inspect ⇒ String
Returns a developer-friendly string representation of the record object.
-
#method_missing(method_name, *args, &block) ⇒ Nacha::Field, Object
Handles dynamic access to fields as methods.
-
#record_type ⇒ Symbol
Returns the record type name for the instance.
-
#respond_to_missing?(method_name) ⇒ Boolean
Complements ‘method_missing` to allow `respond_to?` to work for dynamic field methods.
-
#to_ach ⇒ String
Generates the 94-character ACH string representation of the record.
-
#to_h ⇒ Hash
Returns a hash representation of the record instance.
-
#to_html(_opts = {}) ⇒ String
Generates an HTML representation of the record.
-
#to_json(*_args) ⇒ String
Returns a JSON string representing the record instance.
-
#valid? ⇒ Boolean
Checks if the record is valid by running all validations.
-
#validate ⇒ void
Runs all field-level and record-level validations.
Methods included from Validations::FieldValidations
Constructor Details
#initialize(opts = {}) ⇒ Base
Initializes a new record object.
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`).
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
#children ⇒ Array<Nacha::Record::Base> (readonly)
Returns An array of child records associated with this record.
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)} | </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 |
#fields ⇒ Hash{Symbol => Nacha::Field} (readonly)
Returns 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)} | </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_number ⇒ Object
: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)} | </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 |
#name ⇒ Object (readonly)
Returns the value of attribute name.
24 25 26 |
# File 'lib/nacha/record/base.rb', line 24 def name @name end |
#original_input_line ⇒ String?
Returns 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)} | </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 |
#parent ⇒ Object
: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)} | </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 |
#validations ⇒ Object (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
.definition ⇒ Hash
Returns the field definitions for the record class.
77 78 79 |
# File 'lib/nacha/record/base.rb', line 77 def definition @definition ||= {} end |
.matcher ⇒ Regexp
Generates a Regexp to match against a line to determine if it is of this record type.
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.
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.
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_type ⇒ Symbol
Returns the record type name.
The name is derived from the class name, converted to a snake_case symbol.
157 158 159 |
# File 'lib/nacha/record/base.rb', line 157 def record_type Nacha.record_name(self) end |
.to_h ⇒ Hash
Returns a hash representation of the record class definition.
This includes field definitions and child record types.
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.
184 185 186 |
# File 'lib/nacha/record/base.rb', line 184 def to_json(*_args) JSON.pretty_generate(to_h) end |
.unpack_str ⇒ String
Generates and returns a format string for ‘String#unpack` to parse a record line.
The format string is built based on the field definitions.
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 |
.validations ⇒ Hash
Returns the validation methods for the record class.
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.
399 400 401 |
# File 'lib/nacha/record/base.rb', line 399 def add_error(err_string) @errors << err_string end |
#create_fields_from_definition ⇒ void
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
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.
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
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.
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 |
#definition ⇒ Hash
Returns the field definitions for the record class.
286 287 288 |
# File 'lib/nacha/record/base.rb', line 286 def definition self.class.definition end |
#errors ⇒ Array<String>
Returns all validation errors for the record and its fields.
392 393 394 |
# File 'lib/nacha/record/base.rb', line 392 def errors (@errors + @fields.values.map(&:errors)).flatten end |
#human_name ⇒ String
Returns a human-readable name for the record type.
It converts the snake_case ‘record_type` into a capitalized string with spaces.
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 |
#inspect ⇒ String
Returns a developer-friendly string representation of the record object.
279 280 281 |
# File 'lib/nacha/record/base.rb', line 279 def inspect "#<#{self.class.name}> #{to_h}" end |
#record_type ⇒ Symbol
Returns the record type name for the instance.
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.
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_ach ⇒ String
Generates the 94-character ACH string representation of the record.
This is done by concatenating the ACH string representation of each field.
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_h ⇒ Hash
Returns a hash representation of the record instance.
The hash includes metadata and a representation of each field’s data and errors.
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.
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)} | </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.
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.
305 306 307 308 |
# File 'lib/nacha/record/base.rb', line 305 def valid? validate errors.empty? end |
#validate ⇒ void
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 |