aba_generator 0.3.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f585bf13dfeebd002a78a696ea567080c319aa9fa303ac9c7f207d6d6c097d11
4
- data.tar.gz: 9bad38677f88453966d928493d49328e6c59c4748ca906f662b486351394c919
3
+ metadata.gz: a4ae57548c24971d2f8663b37074f196e43a6b0907253bb26bc939fe92d4ef71
4
+ data.tar.gz: 6fecbc8f643baecc95472091eff95ae4c8300a5a8eb849f6b223cfc55e84f496
5
5
  SHA512:
6
- metadata.gz: eb4183cffed0f03b43c96897e9361db30464cc1fe9b75becbac11442b48c86de2d4eff5f32a15ccd7d53f0ecfb52b773954df8ffaf69b957da3f9057a1ded972
7
- data.tar.gz: 6fd7ddf3f04f5d6f91afdcf18f1faa7f6e72ebac8e671641cf2fac3b6612377b152ac510fb504386772a03a8228256d2ceeedc456b13f9b0f9582326a98fd3cc
6
+ metadata.gz: 652181f14e1716ab1c45d4be01ba7cb7f66efdf9528bf2bacd40ae3d84938b1082e50d17864248ecdef87ed373ef2134be6ba6064e797b216b4733cc5bdcc086
7
+ data.tar.gz: 7a54c370356480de890668e3b1b18b59c7d630ac17059f7eccb0b4b62cf2ae824ff3ff1d89a7e54b221a5b1076d5be2db9492ec67205ff919308ee62205631f3
data/README.md CHANGED
@@ -22,7 +22,28 @@ Or install it yourself as:
22
22
 
23
23
  ## Usage
24
24
 
25
- TODO: Usage instructions to be written.
25
+ ```ruby
26
+ aba = AbaGenerator.new(
27
+ instutition_code: "CBA",
28
+ description: "PAYROLL",
29
+ source_bsb: "123-456",
30
+ source_account_number: "123456789",
31
+ remitter_name: "TEST",
32
+ include_balancing_line: true # defaults to false
33
+ )
34
+
35
+ @payments.each do |payment|
36
+ aba.add_row(
37
+ bsb: '123-456',
38
+ account_number: '1234567',
39
+ account_name: 'J E SMITH',
40
+ amount: 100.00,
41
+ reference: "Payroll #{Time.new.strftime('%m-%d')}"
42
+ )
43
+ end
44
+
45
+ output = aba.generate
46
+ ```
26
47
 
27
48
  ## Development
28
49
 
data/lib/aba_generator.rb CHANGED
@@ -2,9 +2,102 @@ require 'aba_generator/version'
2
2
  require 'ostruct'
3
3
  require 'aba_generator/core_ext/symbol'
4
4
  require 'aba_generator/fixed_width_generator'
5
+ require 'aba_generator/fixed_width/column'
6
+ require 'aba_generator/fixed_width/definition'
7
+ require 'aba_generator/fixed_width/generator'
8
+ require 'aba_generator/fixed_width/parser'
9
+ require 'aba_generator/fixed_width/section'
10
+ require 'aba_generator/header'
11
+ require 'aba_generator/row'
12
+ require 'aba_generator/balancing_line'
13
+ require 'aba_generator/footer'
5
14
 
6
15
  module AbaGenerator
7
16
  class Error < StandardError; end
8
17
 
9
-
18
+ def initialize(options = {})
19
+ # OPTIONS
20
+ # institution_code
21
+ # user_name
22
+ # user_id
23
+ # description
24
+ # source_bsb
25
+ # source_account_number
26
+ # remitter_name
27
+ # include_balancing_line
28
+
29
+ @options = options
30
+ AbaGenerator::Definition.define_fixed_width(@options)
31
+
32
+ results = {}
33
+
34
+ # Define the Header
35
+ results[:header] = AbaGenerator::Header.new(@options)
36
+ results[:body] = []
37
+ end
38
+
39
+ def generate(records)
40
+ AbaGenerator::Definition.define_fixed_width
41
+
42
+ # Create totals
43
+ total = 0
44
+
45
+ # # Create the record rows
46
+ # records.each do |p|
47
+ # results[:body] << {
48
+ # :record_type => 1,
49
+ # :bsb => p.bank_account.bsb_sep,
50
+ # :account_number => ('%9.9s' % p.bank_account.full_account_number),
51
+ # :txcode => 53,
52
+ # :amount => sprintf("%0.10d", (p.amount * 100).to_i),
53
+ # :account_name => p.bank_account.account_name.truncate(32, omission: '').upcase,
54
+ # :reference => "PRIZE #{p.prize.prize_number}",
55
+ # :source_bsb => @options[:source_bsb],
56
+ # :source_account_number => ('%9.9s' % @options[:source_account_number]),
57
+ # :remitter_name => @options[:remitter_name].truncate(16, omission: '')
58
+ # }
59
+ # total += p.amount
60
+ # end
61
+
62
+ # if @options[:include_balancing_line]
63
+ # results[:body] << {
64
+ # :record_type => 1,
65
+ # :bsb => @options[:source_bsb],
66
+ # :account_number => ('%9.9s' % @options[:source_account_number]),
67
+ # :txcode => 13,
68
+ # :amount => sprintf("%0.10d", (total*100).to_i),
69
+ # :account_name => @options[:remitter_name].truncate(16, omission: ''),
70
+ # :reference => @options[:description],
71
+ # :source_bsb => @options[:source_bsb],
72
+ # :source_account_number => ('%9.9s' % @options[:source_account_number]),
73
+ # :remitter_name => @options[:remitter_name].truncate(16, omission: '')
74
+ # }
75
+ # end
76
+
77
+ # # Create the footer, with totals
78
+ # if @options[:include_balancing_line]
79
+ # results[:footer] = [{
80
+ # :record_type => 7,
81
+ # :bsb_filler => "999-999",
82
+ # :net_total => 0,
83
+ # :credit_total => sprintf("%0.10d", (total*100).to_i),
84
+ # :debit_total => sprintf("%0.10d", (total*100).to_i),
85
+ # :records_count => (records.count + 1)
86
+ # }]
87
+ # else
88
+ # results[:footer] = [{
89
+ # :record_type => 7,
90
+ # :bsb_filler => "999-999",
91
+ # :net_total => sprintf("%0.10d", (total*100).to_i),
92
+ # :credit_total => sprintf("%0.10d", (total*100).to_i),
93
+ # :debit_total => 0,
94
+ # :records_count => records.count
95
+ # }]
96
+ # end
97
+
98
+ # Render the output as a string
99
+ FixedWidthGenerator.generate(:aba, results)
100
+
101
+ end
102
+
10
103
  end
@@ -0,0 +1,9 @@
1
+ class AbaGenerator
2
+ class BalancingLine
3
+
4
+ def initialize(options = {})
5
+ @options = options
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,17 @@
1
+ #
2
+ # Taken from ActiveSupport 2.3.5 lib/active_support/core_ext/symbol.rb
3
+ #
4
+ unless :to_proc.respond_to?(:to_proc)
5
+ class Symbol
6
+ # Turns the symbol into a simple proc, which is especially useful for enumerations. Examples:
7
+ #
8
+ # # The same as people.collect { |p| p.name }
9
+ # people.collect(&:name)
10
+ #
11
+ # # The same as people.select { |p| p.manager? }.collect { |p| p.salary }
12
+ # people.select(&:manager?).collect(&:salary)
13
+ def to_proc
14
+ Proc.new { |*args| args.shift.__send__(self, *args) }
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,51 @@
1
+ class AbaGenerator
2
+ class Definition
3
+
4
+ def self.define_fixed_width(options={})
5
+ # Create a FixedWidthGenerator::Defintion to describe ABA file format
6
+ FixedWidthGenerator.define :aba do |d|
7
+ # Create the ABA DESCRIPTION line
8
+ d.header(:align => :left) do |header|
9
+ header.column :record_type, 1, :parser => :to_i
10
+ header.spacer 17
11
+ header.column :reel_sequence_number, 2
12
+ header.column :institution_code, 3
13
+ header.spacer 7
14
+ header.column :user_name, 26
15
+ header.column :user_id, 6, :padding => "0"
16
+ header.column :description, 12
17
+ header.column :process_date, 6
18
+ header.spacer 40
19
+ end
20
+
21
+ d.body(:align => :left) do |body|
22
+ body.column :record_type, 1, :parser => :to_i
23
+ body.column :bsb, 7
24
+ body.column :account_number, 9
25
+ body.column :indicator, 1
26
+ body.column :txcode, 2
27
+ body.column :amount, 10
28
+ body.column :account_name, 32
29
+ body.column :reference, 18
30
+ body.column :source_bsb, 7
31
+ body.column :source_account_number, 9
32
+ body.column :remitter_name, 16
33
+ body.column :withholding_tax_amt, 8, padding: "0"
34
+ end
35
+
36
+ d.footer(:align => :right) do |footer|
37
+ footer.column :record_type, 1, :parser => :to_i
38
+ footer.column :bsb_filler, 7
39
+ footer.spacer 12
40
+ footer.column :net_total, 10, :padding => "0"
41
+ footer.column :credit_total, 10, :padding => "0"
42
+ footer.column :debit_total, 10, :padding => "0"
43
+ footer.spacer 24
44
+ footer.column :records_count, 6, :padding => "0"
45
+ footer.spacer 40
46
+ end
47
+ end
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,85 @@
1
+ class FixedWidthGenerator
2
+ class Column
3
+ DEFAULT_PADDING = ' '
4
+ DEFAULT_ALIGNMENT = :right
5
+ DEFAULT_TRUNCATE = false
6
+ DEFAULT_FORMATTER = :to_s
7
+
8
+ attr_reader :name, :length, :alignment, :padding, :truncate, :group, :unpacker
9
+
10
+ def initialize(name, length, options={})
11
+ assert_valid_options(options)
12
+ @name = name
13
+ @length = length
14
+ @alignment = options[:align] || DEFAULT_ALIGNMENT
15
+ @padding = options[:padding] || DEFAULT_PADDING
16
+ @truncate = options[:truncate] || DEFAULT_TRUNCATE
17
+
18
+ @group = options[:group]
19
+
20
+ @unpacker = "A#{@length}"
21
+
22
+ @parser = options[:parser]
23
+ @parser ||= case @alignment
24
+ when :right then :lstrip
25
+ when :left then :rstrip
26
+ end
27
+ @parser = @parser.to_proc if @parser.is_a?(Symbol)
28
+
29
+ @formatter = options[:formatter]
30
+ @formatter ||= DEFAULT_FORMATTER
31
+ @formatter = @formatter.to_proc if @formatter.is_a?(Symbol)
32
+
33
+ @nil_blank = options[:nil_blank]
34
+ end
35
+
36
+ def parse(value)
37
+ if @nil_blank && blank?(value)
38
+ return nil
39
+ else
40
+ @parser.call(value)
41
+ end
42
+ rescue
43
+ raise ParserError.new("The value '#{value}' could not be parsed: #{$!}")
44
+ end
45
+
46
+ def format(value)
47
+ pad(
48
+ validate_size(
49
+ @formatter.call(value)
50
+ )
51
+ )
52
+ end
53
+
54
+ private
55
+ BLANK_REGEX = /^\s*$/
56
+ def blank?(value)
57
+ value =~ BLANK_REGEX
58
+ end
59
+
60
+ def pad(value)
61
+ case @alignment
62
+ when :left
63
+ value.ljust(@length, @padding)
64
+ when :right
65
+ value.rjust(@length, @padding)
66
+ end
67
+ end
68
+
69
+ def assert_valid_options(options)
70
+ unless options[:align].nil? || [:left, :right].include?(options[:align])
71
+ raise ArgumentError.new("Option :align only accepts :right (default) or :left")
72
+ end
73
+ end
74
+
75
+ def validate_size(result)
76
+ return result if result.length <= @length
77
+ raise FixedWidthGenerator::FormattedStringExceedsLengthError.new(
78
+ "The formatted value '#{result}' in column '#{@name}' exceeds the allowed length of #{@length} chararacters.") unless @truncate
79
+ case @alignment
80
+ when :right then result[-@length,@length]
81
+ when :left then result[0,@length]
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,31 @@
1
+ class FixedWidthGenerator
2
+ class Definition
3
+ attr_reader :sections, :templates, :options
4
+
5
+ def initialize(options={})
6
+ @sections = []
7
+ @templates = {}
8
+ @options = { :align => :right }.merge(options)
9
+ end
10
+
11
+ def section(name, options={}, &block)
12
+ raise DuplicateSectionNameError.new("Duplicate section name: '#{name}'") if @sections.detect{|s| s.name == name }
13
+
14
+ section = FixedWidthGenerator::Section.new(name, @options.merge(options))
15
+ section.definition = self
16
+ yield(section)
17
+ @sections << section
18
+ section
19
+ end
20
+
21
+ def template(name, options={}, &block)
22
+ section = FixedWidthGenerator::Section.new(name, @options.merge(options))
23
+ yield(section)
24
+ @templates[name] = section
25
+ end
26
+
27
+ def method_missing(method, *args, &block)
28
+ section(method, *args, &block)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,20 @@
1
+ class FixedWidthGenerator
2
+ class Generator
3
+
4
+ def initialize(definition)
5
+ @definition = definition
6
+ end
7
+
8
+ def generate(data)
9
+ @builder = []
10
+ @definition.sections.each do |section|
11
+ content = data[section.name]
12
+ arrayed_content = content.is_a?(Array) ? content : [content]
13
+ raise FixedWidthGenerator::RequiredSectionEmptyError.new("Required section '#{section.name}' was empty.") if (content.nil? || content.empty?) && !section.optional
14
+ arrayed_content.each {|row| @builder << section.format(row) }
15
+ end
16
+ @builder.join("\n")
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,47 @@
1
+ class FixedWidthGenerator
2
+ class Parser
3
+ def initialize(definition, file)
4
+ @definition = definition
5
+ @file = file
6
+ end
7
+
8
+ def parse
9
+ @parsed = {}
10
+ @content = read_file
11
+ unless @content.empty?
12
+ @definition.sections.each do |section|
13
+ rows = fill_content(section)
14
+ raise FixedWidthGenerator::RequiredSectionNotFoundError.new("Required section '#{section.name}' was not found.") unless rows > 0 || section.optional
15
+ end
16
+ end
17
+ @parsed
18
+ end
19
+
20
+ private
21
+
22
+ def read_file
23
+ @file.readlines.map(&:chomp)
24
+ end
25
+
26
+ def fill_content(section)
27
+ matches = 0
28
+ loop do
29
+ line = @content.first
30
+ break unless section.match(line)
31
+ add_to_section(section, line)
32
+ matches += 1
33
+ @content.shift
34
+ end
35
+ matches
36
+ end
37
+
38
+ def add_to_section(section, line)
39
+ if section.singular
40
+ @parsed[section.name] = section.parse(line)
41
+ else
42
+ @parsed[section.name] ||= []
43
+ @parsed[section.name] << section.parse(line)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,91 @@
1
+ class FixedWidthGenerator
2
+ class Section
3
+ attr_accessor :definition, :optional, :singular
4
+ attr_reader :name, :columns, :options
5
+
6
+ def initialize(name, options={})
7
+ @name = name
8
+ @options = options
9
+ @columns = []
10
+ @trap = options[:trap]
11
+ @optional = options[:optional] || false
12
+ @singular = options[:singular] || false
13
+ end
14
+
15
+ def column(name, length, options={})
16
+ if column_names_by_group(options[:group]).include?(name)
17
+ raise FixedWidthGenerator::DuplicateColumnNameError.new("You have already defined a column named '#{name}' in the '#{options[:group].inspect}' group.")
18
+ end
19
+ if column_names_by_group(nil).include?(options[:group])
20
+ raise FixedWidthGenerator::DuplicateGroupNameError.new("You have already defined a column named '#{options[:group]}'; you cannot have a group and column of the same name.")
21
+ end
22
+ if group_names.include?(name)
23
+ raise FixedWidthGenerator::DuplicateGroupNameError.new("You have already defined a group named '#{name}'; you cannot have a group and column of the same name.")
24
+ end
25
+
26
+ col = Column.new(name, length, @options.merge(options))
27
+ @columns << col
28
+ col
29
+ end
30
+
31
+ def spacer(length, spacer=nil)
32
+ options = {}
33
+ options[:padding] = spacer if spacer
34
+ column(:spacer, length, options)
35
+ end
36
+
37
+ def trap(&block)
38
+ @trap = block
39
+ end
40
+
41
+ def template(name)
42
+ template = @definition.templates[name]
43
+ raise ArgumentError.new("Template '#{name}' not found as a known template.") unless template
44
+ @columns += template.columns
45
+ # Section options should trump template options
46
+ @options = template.options.merge(@options)
47
+ end
48
+
49
+ def format(data)
50
+ @columns.map do |c|
51
+ hash = c.group ? data[c.group] : data
52
+ c.format(hash[c.name])
53
+ end.join
54
+ end
55
+
56
+ def parse(line)
57
+ line_data = line.unpack(unpacker)
58
+ row = group_names.inject({}) {|h,g| h[g] = {}; h }
59
+
60
+ @columns.each_with_index do |c, i|
61
+ next if c.name == :spacer
62
+ assignee = c.group ? row[c.group] : row
63
+ assignee[c.name] = c.parse(line_data[i])
64
+ end
65
+
66
+ row
67
+ end
68
+
69
+ def match(raw_line)
70
+ raw_line.nil? ? false : @trap.call(raw_line)
71
+ end
72
+
73
+ def method_missing(method, *args)
74
+ column(method, *args)
75
+ end
76
+
77
+ private
78
+
79
+ def column_names_by_group(group)
80
+ @columns.select{|c| c.group == group }.map(&:name) - [:spacer]
81
+ end
82
+
83
+ def group_names
84
+ @columns.map(&:group).compact.uniq
85
+ end
86
+
87
+ def unpacker
88
+ @unpacker ||= @columns.map(&:unpacker).join
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,71 @@
1
+ class FixedWidthGenerator
2
+ class ParserError < RuntimeError; end
3
+ class DuplicateColumnNameError < StandardError; end
4
+ class DuplicateGroupNameError < StandardError; end
5
+ class DuplicateSectionNameError < StandardError; end
6
+ class RequiredSectionNotFoundError < StandardError; end
7
+ class RequiredSectionEmptyError < StandardError; end
8
+ class FormattedStringExceedsLengthError < StandardError; end
9
+ class ColumnMismatchError < StandardError; end
10
+
11
+ #
12
+ # [name] a symbol to reference this file definition later
13
+ # [option] a hash of default options for all sub-elements
14
+ # and a block that defines the sections of the file.
15
+ #
16
+ # returns: +Definition+ instance for this file description.
17
+ #
18
+ def self.define(name, options={}) # yields definition
19
+ definition = Definition.new(options)
20
+ yield(definition)
21
+ definitions[name] = definition
22
+ definition
23
+ end
24
+
25
+ #
26
+ # [data] nested hash describing the contents of the sections
27
+ # [definition_name] symbol +name+ used in +define+
28
+ #
29
+ # returns: string of the transformed +data+ (into fixed-width records).
30
+ #
31
+ def self.generate(definition_name, data)
32
+ definition = definition(definition_name)
33
+ raise ArgumentError.new("Definition name '#{name}' was not found.") unless definition
34
+ generator = Generator.new(definition)
35
+ generator.generate(data)
36
+ end
37
+
38
+ #
39
+ # [file] IO object to write the +generate+d data
40
+ # [definition_name] symbol +name+ used in +define+
41
+ # [data] nested hash describing the contents of the sections
42
+ #
43
+ # writes transformed data to +file+ object as fixed-width records.
44
+ #
45
+ def self.write(file, definition_name, data)
46
+ file.write(generate(definition_name, data))
47
+ end
48
+
49
+ #
50
+ # [file] IO object from which to read the fixed-width text records
51
+ # [definition_name] symbol +name+ used in +define+
52
+ #
53
+ # returns: parsed text records in a nested hash.
54
+ #
55
+ def self.parse(file, definition_name)
56
+ definition = definition(definition_name)
57
+ raise ArgumentError.new("Definition name '#{definition_name}' was not found.") unless definition
58
+ parser = Parser.new(definition, file)
59
+ parser.parse
60
+ end
61
+
62
+ private
63
+
64
+ def self.definitions
65
+ @@definitions ||= {}
66
+ end
67
+
68
+ def self.definition(name)
69
+ definitions[name]
70
+ end
71
+ end
@@ -0,0 +1,9 @@
1
+ class AbaGenerator
2
+ class Footer
3
+
4
+ def initialize(options = {})
5
+ @options = options
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,17 @@
1
+ class AbaGenerator
2
+ class Header
3
+
4
+ def initialize(options = {})
5
+ [{
6
+ :record_type => 0,
7
+ :reel_sequence_number => "01",
8
+ :institution_code => options[:institution_code],
9
+ :user_name => options[:user_name],
10
+ :user_id => (options[:user_id] || '000000'),
11
+ :description => options[:description],
12
+ :process_date => Time.new.strftime("%d%m%y")
13
+ }]
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,9 @@
1
+ class AbaGenerator
2
+ class Row
3
+
4
+ def initialize(options = {})
5
+ @options = options
6
+ end
7
+
8
+ end
9
+ end
@@ -1,3 +1,3 @@
1
1
  module AbaGenerator
2
- VERSION = "0.3.1"
2
+ VERSION = "1.0.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aba_generator
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Keegan Bakker
@@ -27,6 +27,18 @@ files:
27
27
  - bin/console
28
28
  - bin/setup
29
29
  - lib/aba_generator.rb
30
+ - lib/aba_generator/balancing_line.rb
31
+ - lib/aba_generator/core_ext/symbol.rb
32
+ - lib/aba_generator/definition.rb
33
+ - lib/aba_generator/fixed_width/column.rb
34
+ - lib/aba_generator/fixed_width/definition.rb
35
+ - lib/aba_generator/fixed_width/generator.rb
36
+ - lib/aba_generator/fixed_width/parser.rb
37
+ - lib/aba_generator/fixed_width/section.rb
38
+ - lib/aba_generator/fixed_width_generator.rb
39
+ - lib/aba_generator/footer.rb
40
+ - lib/aba_generator/header.rb
41
+ - lib/aba_generator/row.rb
30
42
  - lib/aba_generator/version.rb
31
43
  homepage: https://github.com/audata/aba_generator
32
44
  licenses: