ach_builder 0.0.2 → 0.2.1

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.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.rspec +2 -1
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +10 -0
  7. data/Gemfile +11 -2
  8. data/MIT-LICENSE +1 -1
  9. data/README.md +19 -11
  10. data/Rakefile +44 -0
  11. data/ach_builder.gemspec +9 -5
  12. data/lib/ach/batch.rb +10 -32
  13. data/lib/ach/batch/builder.rb +60 -0
  14. data/lib/ach/batch/control.rb +18 -3
  15. data/lib/ach/batch/header.rb +21 -4
  16. data/lib/ach/component.rb +125 -43
  17. data/lib/ach/component/has_many_association.rb +155 -0
  18. data/lib/ach/constants.rb +15 -1
  19. data/lib/ach/file.rb +50 -44
  20. data/lib/ach/file/builder.rb +81 -0
  21. data/lib/ach/file/control.rb +15 -3
  22. data/lib/ach/file/header.rb +20 -3
  23. data/lib/ach/file/reader.rb +103 -0
  24. data/lib/ach/file/transmission_header.rb +77 -0
  25. data/lib/ach/formatter.rb +113 -76
  26. data/lib/ach/formatter/rule.rb +27 -0
  27. data/lib/ach/record.rb +13 -64
  28. data/lib/ach/record/addenda.rb +25 -0
  29. data/lib/ach/record/base.rb +109 -0
  30. data/lib/ach/record/dynamic.rb +58 -0
  31. data/lib/ach/record/entry.rb +49 -0
  32. data/lib/ach/record/tail.rb +10 -0
  33. data/lib/ach/validations.rb +5 -3
  34. data/lib/ach/version.rb +1 -1
  35. data/lib/ach_builder.rb +20 -33
  36. data/spec/batch_spec.rb +23 -11
  37. data/spec/componenet/has_many_association_spec.rb +111 -0
  38. data/spec/file_spec.rb +173 -81
  39. data/spec/formatter_spec.rb +9 -9
  40. data/spec/reader_spec.rb +55 -0
  41. data/spec/record/addenda_spec.rb +8 -0
  42. data/spec/record/base_spec.rb +53 -0
  43. data/spec/record/dynamic_spec.rb +74 -0
  44. data/spec/record/entry_spec.rb +7 -0
  45. data/spec/record/tail_spec.rb +7 -0
  46. data/spec/spec_helper.rb +1 -0
  47. data/spec/support/ach_file_factory.rb +70 -0
  48. data/spec/support/examples/well_fargo_empty.ach +2 -0
  49. data/spec/support/examples/well_fargo_with_data.ach +6 -0
  50. data/spec/support/helpers/ach_files_examples.rb +17 -0
  51. metadata +69 -26
  52. data/lib/ach/entry.rb +0 -31
  53. data/lib/ach/tail.rb +0 -6
  54. data/spec/entry_spec.rb +0 -7
  55. data/spec/record_spec.rb +0 -34
  56. data/spec/tail_spec.rb +0 -7
@@ -0,0 +1,77 @@
1
+ module ACH
2
+ # Hosts functionality required to append +TransmissionHeader+ to a file.
3
+ # +TransmissionHeader+ is optional and inherited from <tt>ACH::Record::Dynamic</tt>
4
+ # class, which means it may have variable number of fields with custom formatting.
5
+ # +TransmissionHeader+ may be defined only once per file. You may specify default
6
+ # value for custom fields during definition
7
+ #
8
+ # == Example
9
+ #
10
+ # class MyFile < ACH::File
11
+ # trasmission_header do
12
+ # starting '->1' => '<'
13
+ # receiver_name '->10'
14
+ # ending '->1' => '>'
15
+ # end
16
+ # # other definitions
17
+ # end
18
+ #
19
+ # file = MyFile.new do
20
+ # receiver_name 'MY PROVIDER'
21
+ # end
22
+ module File::TransmissionHeader
23
+ extend ActiveSupport::Concern
24
+
25
+ # Raised when (descendant of) ACH File tries to redeclare it's +TransmissionHeader+
26
+ class RedefinedTransmissionHeaderError < RuntimeError
27
+ def initialize
28
+ super "TransmissionHeader record may be defined only once"
29
+ end
30
+ end
31
+
32
+ # Raised when +TransmissionHeader+ is declared with no fields in it
33
+ class EmptyTransmissionHeaderError < RuntimeError
34
+ def initialize
35
+ super "Transmission_header should declare it's fields"
36
+ end
37
+ end
38
+
39
+ module ClassMethods
40
+ # Defines and declares +TransmissionHeader+ class within scope of +self+
41
+ def transmission_header(&block)
42
+ raise RedefinedTransmissionHeaderError if have_transmission_header?
43
+
44
+ klass = Class.new(Record::Dynamic, &block)
45
+
46
+ raise EmptyTransmissionHeaderError if klass.fields.nil? || klass.fields.empty?
47
+
48
+ const_set(:TransmissionHeader, klass)
49
+ @have_transmission_header = true
50
+ end
51
+
52
+ # Returns +true+ if +TransmissionHeader+ is defined within scope of the class.
53
+ def have_transmission_header?
54
+ @have_transmission_header
55
+ end
56
+ end
57
+
58
+ # Helper instance method. Returns +true+ if +TransmissionHeader+ is defined within
59
+ # scope of it's class
60
+ def have_transmission_header?
61
+ self.class.have_transmission_header?
62
+ end
63
+
64
+ # Builds +TransmissionHeader+ record for self. Yields it to +block+, if passed.
65
+ # Returns nil if no +TransmissionHeader+ is defined within scope of class.
66
+ def transmission_header(fields = {}, &block)
67
+ return nil unless have_transmission_header?
68
+
69
+ merged_fields = fields_for(self.class::TransmissionHeader).merge(fields)
70
+
71
+ @transmission_header ||= self.class::TransmissionHeader.new(merged_fields)
72
+ @transmission_header.tap do |head|
73
+ head.instance_eval(&block) if block
74
+ end
75
+ end
76
+ end
77
+ end
@@ -1,84 +1,121 @@
1
1
  module ACH
2
+ # Every field should be formatted with its own rule so Formatter take care about it.
3
+ # Rules are defined in ACH::RULES constant.
4
+ #
5
+ # == Rule Format
6
+ #
7
+ # Every rule can contain the next items:
8
+ # * _justification_ - "<-" or "->".
9
+ # * _width_ - a number of characters for a field.
10
+ # * _padding_ - specifying "-" will pad right-justified values with spaces instead of zeros.
11
+ # * _transformation_ - allows to call method on a string. Pipe must precedes a method name. Foe example: "|upcase".
12
+ # For real examples see RULES constants.
13
+ #
14
+ # == Usage Example
15
+ #
16
+ # ACH::Formatter.format(:customer_name, "LINUS TORVALDS") # => "LINUS TORVALDS "
17
+ # ACH::Formatter.format(:amount, 52) # => "0000000052"
18
+ # ACH::Formatter.customer_acct('1234567890') # => "1234567890 "
2
19
  module Formatter
3
- extend self
4
-
20
+ extend ActiveSupport::Autoload
21
+
22
+ autoload :Rule
23
+
24
+ # Rules for formatting each field. See module documentation for examples.
5
25
  RULES = {
6
- :customer_name => '<-22',
7
- :customer_acct => '<-15',
8
- :amount => '->10',
9
- :bank_2 => '<-2',
10
- :transaction_type => '<-2',
11
- :bank_15 => '<-15',
12
- :addenda => '<-1',
13
- :trace_num => '<-15',
14
- :transaction_code => '<-2',
15
- :record_type => '<-1',
16
- :bank_account => '<-17',
17
- :routing_number => '->9',
18
- :priority_code => '->2',
19
- :immediate_dest => '->10-',
20
- :immediate_origin => '->10-',
21
- :date => '<-6',
22
- :time => '<-4',
23
- :file_id_modifier => '<-1|upcase',
24
- :record_size => '->3',
25
- :blocking_factor => '->2',
26
- :format_code => '<-1',
27
- :immediate_dest_name => '<-23',
28
- :immediate_origin_name => '<-23',
29
- :reference_code => '<-8',
30
- :service_class_code => '<-3',
31
- :company_name => '<-16',
32
- :company_note_data => '<-20',
33
- :company_id => '<-10',
34
- :entry_class_code => '<-3',
35
- :company_entry_descr => '<-10',
36
- :effective_date => '<-6',
37
- :settlement_date => '<-3',
38
- :origin_status_code => '<-1',
39
- :origin_dfi_id => '<-8',
40
- :batch_number => '->7',
41
- :entry_count => '->6',
42
- :entry_hash => '->10',
43
- :total_debit_amount => '->12',
44
- :total_credit_amount => '->12',
45
- :authen_code => '<-19',
46
- :bank_6 => '<-6',
47
- :batch_count => '->6',
48
- :block_count => '->6',
49
- :file_entry_count => '->8',
50
- :bank_39 => '<-39',
51
- :nines => '<-94'
52
- }.freeze
53
-
54
- RULE_PARSER_REGEX = /^(<-|->)(\d+)(-)?(\|\w+)?$/
55
-
56
- @@compiled_rules = {}
57
-
58
- def format field_name, value
59
- compile_rule(field_name) unless @@compiled_rules.key?(field_name)
60
- @@compiled_rules[field_name].call(value)
26
+ :customer_name => '<-22',
27
+ :customer_acct => '<-15',
28
+ :amount => '->10',
29
+ :bank_2 => '<-2',
30
+ :transaction_type => '<-2',
31
+ :bank_15 => '<-15',
32
+ :addenda => '<-1',
33
+ :trace_num => '<-15',
34
+ :transaction_code => '<-2',
35
+ :record_type => '<-1',
36
+ :bank_account => '<-17',
37
+ :routing_number => '->9',
38
+ :priority_code => '->2',
39
+ :immediate_dest => '->10-',
40
+ :immediate_origin => '->10-',
41
+ :date => '<-6',
42
+ :time => '<-4',
43
+ :file_id_modifier => '<-1|upcase',
44
+ :record_size => '->3',
45
+ :blocking_factor => '->2',
46
+ :format_code => '<-1',
47
+ :immediate_dest_name => '<-23',
48
+ :immediate_origin_name => '<-23',
49
+ :reference_code => '<-8',
50
+ :service_class_code => '<-3',
51
+ :company_name => '<-16', # aka Individual Name
52
+ :company_note_data => '<-20',
53
+ :company_id => '<-10',
54
+ :entry_class_code => '<-3',
55
+ :company_entry_descr => '<-10',
56
+ :effective_date => '<-6',
57
+ :settlement_date => '<-3',
58
+ :origin_status_code => '<-1',
59
+ :origin_dfi_id => '<-8',
60
+ :batch_number => '->7',
61
+ :entry_addenda_count => '->6',
62
+ :entry_hash => '->10',
63
+ :total_debit_amount => '->12',
64
+ :total_credit_amount => '->12',
65
+ :authen_code => '<-19',
66
+ :bank_6 => '<-6',
67
+ :batch_count => '->6',
68
+ :block_count => '->6',
69
+ :file_entry_addenda_count => '->8',
70
+ :bank_39 => '<-39',
71
+ :nines => '<-94',
72
+
73
+ # Batch header
74
+ :desc_date => '<-6-', # company descriptive date
75
+
76
+ # Addenda Record
77
+ :addenda_type_code => '->2',
78
+ :payment_related_info => '<-80',
79
+ :addenda_sequence_num => '->4',
80
+ :entry_details_sequence_num => '->7'
81
+ }
82
+
83
+ # Returns +true+ if +field_name+ is one of the keys of +RULES+
84
+ def self.defined?(field_name)
85
+ RULES.key?(field_name)
61
86
  end
62
-
63
- def method_missing meth, *args
64
- if RULES.key? meth
65
- format meth, *args
66
- else
67
- super
68
- end
87
+
88
+ # Adds +field+ with corresponding +format+ to +RULES+
89
+ def self.define(field, format)
90
+ RULES[field] = format
69
91
  end
70
-
71
- def compile_rule field_name
72
- just, width, pad, transf = RULES[field_name].match(RULE_PARSER_REGEX)[1..-1]
73
- padmethod = just == '<-' ? :ljust : :rjust
74
- length = width.to_i
75
- padstr = padmethod == :ljust ? ' ' : pad == '-' ? ' ' : '0'
76
- transform = transf[1..-1] if transf
77
- @@compiled_rules[field_name] = lambda{ |val|
78
- val = val.to_s[0..length]
79
- (transform ? val.send(transform) : val).send(padmethod, length, padstr)
80
- }
92
+
93
+ # If missing method name is one of the defined rules, passes its name
94
+ # and the rest of arguments to the +format+ method
95
+ def self.method_missing(meth, *args)
96
+ self.defined?(meth) ? format(meth, *args) : super
97
+ end
98
+
99
+ # Formats +value+ using the rule defined by +field_name+ format
100
+ def self.format(field_name, value)
101
+ rule_for_field(field_name).call(value)
102
+ end
103
+
104
+ # Returns ACH::Formatter::Rule rule, built from the corresponding format
105
+ # definition of a +field+
106
+ def self.rule_for_field(field)
107
+ compiled_rules[field] ||= Rule.new(RULES[field])
108
+ end
109
+
110
+ # Returns a hash of all rules that have been built at the moment of method call
111
+ def self.compiled_rules
112
+ @compiled_rules ||= {}
113
+ end
114
+
115
+ # Returns a regular expression that can be used to split string into matched parts,
116
+ # that will correspond to passed +fields+ parameter. Used for ACH reading purposes
117
+ def self.matcher_for(fields)
118
+ /^#{fields.map{ |f| "(.{#{rule_for_field(f).length}})" }.join}$/
81
119
  end
82
- private :compile_rule
83
120
  end
84
121
  end
@@ -0,0 +1,27 @@
1
+ module ACH
2
+ # Parses string representation of rule and builds a +Proc+ based on it
3
+ class Formatter::Rule
4
+ # Captures formatting tokens from a rule string.
5
+ RULE_PARSER_REGEX = /^(<-|->)(\d+)(-)?(\|\w+)?$/
6
+
7
+ delegate :call, :[], :to => :@lambda
8
+
9
+ attr_reader :length
10
+
11
+ # Initializes instance with formatting data. Parses passed string for formatting
12
+ # values, such as width, justification, etc. As the result, builds a Proc object
13
+ # that will be used to format passed string according to formatting rule.
14
+ def initialize(rule)
15
+ just, width, pad, transf = rule.match(RULE_PARSER_REGEX)[1..-1]
16
+ @length = width.to_i
17
+ @padmethod = just == '<-' ? :ljust : :rjust
18
+ @padstr = @padmethod == :ljust ? ' ' : pad == '-' ? ' ' : '0'
19
+ @transform = transf[1..-1] if transf
20
+
21
+ @lambda = Proc.new do |val|
22
+ val = val.to_s
23
+ (@transform ? val.send(@transform) : val).send(@padmethod, @length, @padstr)[-@length..-1]
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,66 +1,15 @@
1
1
  module ACH
2
- class Record
3
- include Validations
4
- include Constants
5
-
6
- class UnknownField < ArgumentError
7
- def initialize field, class_name
8
- super "Unrecognized field '#{field}' in class #{class_name}"
9
- end
10
- end
11
-
12
- class EmptyField < ArgumentError
13
- def initialize field
14
- super "Empty field '#{field}' for #{inspect}"
15
- end
16
- end
17
-
18
- def self.fields *field_names
19
- return @fields if field_names.empty?
20
- @fields = field_names
21
- @fields.each do |field|
22
- raise UnknownField.new(field, name) unless Formatter::RULES.key?(field)
23
-
24
- define_method(field) do |*args|
25
- args.empty? ? fields[field] : (fields[field] = args.first)
26
- end
27
-
28
- define_method("#{field}=") do |val|
29
- fields[field] = val
30
- end
31
- end
32
- end
33
-
34
- def self.defaults default_values = nil
35
- return @defaults if default_values.nil?
36
- @defaults = default_values.freeze
37
- end
38
-
39
- def initialize fields = {}, &block
40
- defaults.each do |key, value|
41
- self.fields[key] = Proc === value ? value.call : value
42
- end
43
- self.fields.merge!(fields)
44
- instance_eval(&block) if block
45
- end
46
-
47
- def defaults
48
- self.class.defaults
49
- end
50
-
51
- def fields
52
- @fields ||= {}
53
- end
54
-
55
- def []= name, val
56
- fields[name] = val
57
- end
58
-
59
- def to_s!
60
- self.class.fields.map do |name|
61
- raise EmptyField.new(name) if @fields[name].nil?
62
- Formatter.format name, @fields[name]
63
- end.join
64
- end
2
+ # Responsible for record-specific functionality for most record types,
3
+ # handled by several subclasses. Record types handled outside this module
4
+ # include: ACH::File::Header, ACH::File::Control, ACH::Batch::Header,
5
+ # ACH::Batch::Control
6
+ module Record
7
+ extend ActiveSupport::Autoload
8
+
9
+ autoload :Addenda
10
+ autoload :Base
11
+ autoload :Dynamic
12
+ autoload :Entry
13
+ autoload :Tail
65
14
  end
66
- end
15
+ end
@@ -0,0 +1,25 @@
1
+ module ACH
2
+ module Record
3
+ # An ACH::Record::Addenda is an ACH::Record::Base record located inside of
4
+ # an ACH::Batch component. An addenda record should be preceded by an
5
+ # ACH::Record::Entry record.
6
+ #
7
+ # == Fields
8
+ #
9
+ # * record_type
10
+ # * addenda_type_code
11
+ # * payment_related_info
12
+ # * addenda_sequence_num
13
+ # * entry_details_sequence_num
14
+ class Addenda < Base
15
+ fields :record_type,
16
+ :addenda_type_code,
17
+ :payment_related_info,
18
+ :addenda_sequence_num,
19
+ :entry_details_sequence_num
20
+
21
+ defaults :record_type => BATCH_ADDENDA_RECORD_TYPE,
22
+ :addenda_type_code => 5
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,109 @@
1
+ module ACH
2
+ module Record
3
+ # Base class for all record entities (e.g. +ACH::File::Header+,
4
+ # +ACH::File::Control+, +ACH::Record::Entry+, others). Any record
5
+ # being declared should specify its fields, and optional default values.
6
+ # Except for +ACH::Record::Dynamic+, any declared field within a record
7
+ # should have corresponding rule defined under +ACH::Rule::Formatter+.
8
+ #
9
+ # == Example
10
+ #
11
+ # class Addenda < Record
12
+ # fields :record_type,
13
+ # :addenda_type_code,
14
+ # :payment_related_info,
15
+ # :addenda_sequence_num,
16
+ # :entry_details_sequence_num
17
+ #
18
+ # defaults :record_type => 7
19
+ # end
20
+ #
21
+ # addenda = ACH::Addenda.new(
22
+ # :addenda_type_code => '05',
23
+ # :payment_related_info => 'PAYMENT_RELATED_INFO',
24
+ # :addenda_sequence_num => 1,
25
+ # :entry_details_sequence_num => 1 )
26
+ # addenda.to_s! # => "705PAYMENT_RELATED_INFO 00010000001"
27
+ class Base
28
+ include Validations
29
+ include Constants
30
+
31
+ # Raises when unknown field passed to ACH::Record::Base.fields method.
32
+ class UnknownFieldError < ArgumentError
33
+ def initialize(field, class_name)
34
+ super "Unrecognized field '#{field}' in class #{class_name}"
35
+ end
36
+ end
37
+
38
+ # Raises when value of record's field is not specified and there is no
39
+ # default value.
40
+ class EmptyFieldError < ArgumentError
41
+ def initialize(field, record)
42
+ super "Empty field '#{field}' for #{record}"
43
+ end
44
+ end
45
+
46
+ # Specifies fields of the record. Order is important. All fields
47
+ # must be declared in ACH::Formatter +RULES+. See class description
48
+ # for example
49
+ def self.fields(*field_names)
50
+ return @fields if field_names.empty?
51
+ @fields = field_names
52
+ @fields.each{ |field| define_field_methods(field) }
53
+ end
54
+
55
+ # Sets default values for fields. See class description for example
56
+ def self.defaults(default_values = nil)
57
+ return @defaults if default_values.nil?
58
+ @defaults = default_values.freeze
59
+ end
60
+
61
+ def self.define_field_methods(field)
62
+ raise UnknownFieldError.new(field, name) unless Formatter::RULES.key?(field)
63
+ define_method(field) do |*args|
64
+ args.empty? ? @fields[field] : (@fields[field] = args.first)
65
+ end
66
+ define_method("#{field}=") do |val|
67
+ @fields[field] = val
68
+ end
69
+ end
70
+ private_class_method :define_field_methods
71
+
72
+ def self.from_s(string)
73
+ field_matcher_regexp = Formatter.matcher_for(fields)
74
+ new Hash[*fields.zip(string.match(field_matcher_regexp)[1..-1]).flatten]
75
+ end
76
+
77
+ def initialize(fields = {}, &block)
78
+ defaults.each do |key, value|
79
+ self.fields[key] = Proc === value ? value.call : value
80
+ end
81
+ self.fields.merge!(fields)
82
+ instance_eval(&block) if block
83
+ end
84
+
85
+ # Builds a string from record object.
86
+ def to_s!
87
+ self.class.fields.map do |name|
88
+ raise EmptyFieldError.new(name, self) if @fields[name].nil?
89
+ Formatter.format name, @fields[name]
90
+ end.join
91
+ end
92
+
93
+ # Returns a hash where key is field's name and value is field's value.
94
+ def fields
95
+ @fields ||= {}
96
+ end
97
+
98
+ def defaults
99
+ self.class.defaults
100
+ end
101
+ private :defaults
102
+
103
+ def []=(name, val)
104
+ fields[name] = val
105
+ end
106
+ private :[]=
107
+ end
108
+ end
109
+ end