lib/aba/batch.rb

Summary

Maintainability
A
1 hr
Test Coverage
# frozen_string_literal: true

class Aba
  class Batch
    include Aba::Validations

    attr_accessor :bsb, :financial_institution, :user_name, :user_id, :description, :process_at, :entries

    # BSB
    validates_bsb         :bsb, allow_blank: true

    # Financial Institution
    validates_length      :financial_institution, 3

    # User Name
    validates_presence_of :user_name
    validates_max_length  :user_name, 26
    validates_becs        :user_name

    # User ID
    validates_presence_of :user_id
    validates_max_length  :user_id, 6
    validates_integer     :user_id, false

    # Description
    validates_max_length  :description, 12
    validates_becs        :description

    # Process at Date
    validates_length      :process_at, 6
    validates_integer     :process_at, false

    # Totals
    validates_max_length  :debit_total_amount, 10
    validates_max_length  :credit_total_amount, 10

    def initialize(attrs = {}, transactions = [])
      attrs.each do |key, value|
        send("#{key}=", value)
      end

      @entries = []

      unless transactions.nil? || transactions.empty?
        transactions.to_a.each do |t|
          self.add_transaction(t) unless t.nil? || t.empty?
        end
      end

      yield self if block_given?
    end

    def to_s
      raise RuntimeError, 'No entries present - add one using `add_transaction` or `add_return`' if entries.empty?
      raise RuntimeError, 'ABA data is invalid - check the contents of `errors`' unless valid?

      # Descriptive record
      output = "#{descriptive_record}\r\n"

      # Transactions records
      output += entries.map { |t| t.to_s }.join("\r\n")

      # Batch control record
      output += "\r\n#{batch_control_record}"
    end

    def add_transaction(attrs = {})
      add_entry(Aba::Transaction, attrs)
    end

    def add_return(attrs = {})
      add_entry(Aba::Return, attrs)
    end

    def transactions
      entries.select { |entry| entry.instance_of?(Aba::Transaction) }
    end

    def transactions_valid?
      !transactions.map { |t| t.valid? }.include?(false)
    end

    def valid?
      !has_errors? && !has_entry_errors?
    end

    def errors
      # Run validations
      has_errors?
      has_entry_errors?

      # Build errors
      all_errors = {}
      all_errors[:aba] = self.error_collection unless self.error_collection.empty?
      entry_error_collection = entries.each_with_index
        .map { |t, i| [i, t.error_collection] }
        .reject { |e| e[1].nil? || e[1].empty? }
        .to_h
      all_errors[:entries] = entry_error_collection unless entry_error_collection.empty?

      all_errors unless all_errors.empty?
    end

    private

    def add_entry(type, attrs)
      (attrs.instance_of?(type) ? attrs : type.new(attrs)).tap do |entry|
        entries << entry
      end
    end

    def has_entry_errors?
      entries.map { |t| t.valid? }.include?(false)
    end

    def descriptive_record
      # Record type
      # Max: 1
      # Char position: 1
      output = "0"

      # Optional branch number of the funds account with a hyphen in the 4th character position
      # Char position: 2-18
      # Max: 17
      # Blank filled
      output += self.bsb.nil? ? " " * 17 : self.bsb.to_s.ljust(17)

      # Sequence number
      # Char position: 19-20
      # Max: 2
      # Zero padded
      output += "01"

      # Name of user financial instituion
      # Max: 3
      # Char position: 21-23
      output += self.financial_institution.to_s

      # Reserved
      # Max: 7
      # Char position: 24-30
      output += " " * 7

      # Name of User supplying File
      # Char position: 31-56
      # Max: 26
      # Full BECS character set valid
      # Blank filled
      output += self.user_name.to_s.ljust(26)

      # Direct Entry User ID
      # Char position: 57-62
      # Max: 6
      # Zero padded
      output += self.user_id.to_s.rjust(6, "0")

      # Description of payments in the file (e.g. Payroll, Creditors etc.)
      # Char position: 63-74
      # Max: 12
      # Full BECS character set valid
      # Blank filled
      output += self.description.to_s.ljust(12)

      # Date on which the payment is to be processed
      # Char position: 75-80
      # Max: 6
      output += self.process_at.to_s.rjust(6, "0")

      # Reserved
      # Max: 40
      # Char position: 81-120
      output += " " * 40
    end

    def batch_control_record
      cached_credit_total_amount = credit_total_amount
      cached_debit_total_amount = debit_total_amount

      # Record type
      # Max: 1
      # Char position: 1
      output = "7"

      # BSB Format Filler
      # Max: 7
      # Char position: 2-8
      output += "999-999"

      # Reserved
      # Max: 12
      # Char position: 9-20
      output += " " * 12

      # Net total
      # Max: 10
      # Char position: 21-30
      output += (cached_credit_total_amount - cached_debit_total_amount).abs.to_s.rjust(10, "0")

      # Credit Total Amount
      # Max: 10
      # Char position: 31-40
      output += cached_credit_total_amount.to_s.rjust(10, "0")

      # Debit Total Amount
      # Max: 10
      # Char position: 41-50
      output += cached_debit_total_amount.to_s.rjust(10, "0")

      # Reserved
      # Max: 24
      # Char position: 51-74
      output += " " * 24

      # Total Item Count
      # Max: 6
      # Char position: 75-80
      output += entries.size.to_s.rjust(6, "0")

      # Reserved
      # Max: 40
      # Char position: 81-120
      output += " " * 40
    end

    def credit_total_amount
      credit_total_amount ||= entries.reject(&:debit?).sum(&method(:entry_amount))
    end

    def debit_total_amount
      entries.select(&:debit?).sum(&method(:entry_amount))
    end

    def entry_amount(entry)
      entry.amount.to_s[/\A\-?\d+\z/] ? Integer(entry.amount).abs : 0
    end
  end
end