aba_generator 0.3.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: