ach_builder 0.0.2 → 0.2.1

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