metro2_format 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 86d8af24ca1737814501fd020751719885ec7d49c93a54e9774feebecc142250
4
+ data.tar.gz: b3756ce2a4ebc029957419bb7cdb168d2ceb4813688354ee927362720d0329c4
5
+ SHA512:
6
+ metadata.gz: 293d93ccdf56965eb7cf5619e17477418e9e1b7f0df2334cd59aed173a0ca0352c5bceeddded93d70774f0debc6362fddef31fa873294cb9a00e936d59d31e17
7
+ data.tar.gz: 523566fc302e17ad6b74ca3b169b14f35b3810e29410d3c7dc8d77185bba3a39e5f5181dbbc83ac50fc11368df389f77e7a07c86469d773f25427fa52006c0c0
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --colour
2
+
3
+ --format documentation
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in metro_2.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Brian Mascarenhas
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # Metro2
2
+
3
+ Creates files in Metro 2 format for reporting to credit agencies
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'metro_2'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install metro_2
20
+
21
+ ## Usage
22
+
23
+ ```ruby
24
+ metro_2_content = Metro2::Metro2File.new
25
+ metro_2_content.header.cycle_number = 15
26
+ metro_2_content.header.equifax_program_identifier = 'EFAXID'
27
+ metro_2_content.header.transunion_program_identifier = 'TRANSUNION'
28
+ # ... other header segment attributes
29
+
30
+ base_segment = Metro2::Records::BaseSegment.new
31
+ base_segment.time_stamp = Time.new(2014, 9, 15, 17, 7, 45)
32
+ base_segment.identification_number = 'REPORTERXYZ'
33
+ base_segment.cycle_number = 1
34
+ base_segment.consumer_account_number = 'ABC123'
35
+ base_segment.portfolio_type = 'I'
36
+ # ... other base segment attributes
37
+ metro_2_content.base_segments << base_segment
38
+ # add more base segments as needed
39
+
40
+ metro_2_content.to_s # contents to write to file
41
+ ```
42
+
43
+ ## Contributing
44
+
45
+ 1. Fork it ( https://github.com/[my-github-username]/metro_2/fork )
46
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
47
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
48
+ 4. Push to the branch (`git push origin my-new-feature`)
49
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env rake
2
+
3
+ require 'rubygems'
4
+ require 'bundler/setup'
5
+ Bundler.require
6
+ require 'metro_2/version'
7
+
8
+ require 'rspec/core/rake_task'
9
+
10
+ RSpec::Core::RakeTask.new(:spec)
11
+
12
+ task :default => :spec
13
+
14
+ task :build do
15
+ system 'gem build metro_2.gemspec'
16
+ end
17
+
data/lib/metro_2.rb ADDED
@@ -0,0 +1,241 @@
1
+ require 'metro_2/version'
2
+
3
+ module Metro2
4
+
5
+ PORTFOLIO_TYPE = {
6
+ line_of_credit: 'C',
7
+ installment: 'I',
8
+ mortgage: 'M',
9
+ open_account: 'O',
10
+ revolving: 'R'
11
+ }.freeze
12
+
13
+ ACCOUNT_TYPE = {
14
+ unsecured: '01',
15
+ education: '12'
16
+ # TODO: add other account types
17
+ }.freeze
18
+
19
+ ECOA_CODE = {
20
+ individual: '1',
21
+ deceased: 'X',
22
+ delete: 'Z'
23
+ # TODO: add other ECOA codes
24
+ }.freeze
25
+
26
+ SPECIAL_COMMENT_CODE = {
27
+ partial_payment_agreement: 'AC',
28
+ purchased_by_another_company: 'AH',
29
+ paid_in_full_less_than_full_balance: 'AU',
30
+ affected_by_natural_or_declared_disaster: 'AW',
31
+ loan_modified: 'CO',
32
+ forbearance: 'CP',
33
+ # TODO: add other special comment codes
34
+ }.freeze
35
+
36
+ COMPLIANCE_CONDITION_CODE = {
37
+ in_dispute: 'XB',
38
+ previously_in_dispute_now_resolved: 'XH',
39
+ # TODO: add other compliance condition codes
40
+ }.freeze
41
+
42
+ INTEREST_TYPE_INDICATOR = {
43
+ fixed: 'F',
44
+ variable: 'V'
45
+ }.freeze
46
+
47
+ CORRECTION_INDICATOR = '1'
48
+
49
+ TERMS_FREQUENCY = {
50
+ deferred: 'D',
51
+ single_payment: 'P',
52
+ weekly: 'W',
53
+ biweekly: 'B',
54
+ semimonthly: 'S',
55
+ monthly: 'M',
56
+ bimonthly: 'L',
57
+ quarterly: 'Q',
58
+ triannually: 'T',
59
+ semiannually: 'S',
60
+ annually: 'Y'
61
+ }.freeze
62
+
63
+ ACCOUNT_STATUS = {
64
+ account_transferred: '05',
65
+ current: '11',
66
+ closed: '13',
67
+ paid_in_full_voluntary_surrender: '61',
68
+ paid_in_full_collection_account: '62',
69
+ paid_in_full_repossession: '63',
70
+ paid_in_full_charge_off: '64',
71
+ paid_in_full_foreclosure: '65',
72
+ past_due_30_59: '71',
73
+ past_due_60_89: '78',
74
+ past_due_90_119: '80',
75
+ past_due_120_149: '82',
76
+ past_due_150_179: '83',
77
+ past_due_180_plus: '84',
78
+ govt_insurance_claim_filed: '88',
79
+ deed_received: '89',
80
+ collections: '93',
81
+ foreclosure_completed: '94',
82
+ voluntary_surrender: '95',
83
+ merch_repossessed: '96',
84
+ charge_off: '97',
85
+ delete_account: 'DA',
86
+ delete_account_fraud: 'DF'
87
+ }.freeze
88
+
89
+ PAYMENT_HISTORY_PROFILE = {
90
+ current: '0',
91
+ past_due_30_59: '1',
92
+ past_due_60_89: '2',
93
+ past_due_90_119: '3',
94
+ past_due_120_149: '4',
95
+ past_due_150_179: '5',
96
+ past_due_180_plus: '6',
97
+ no_history_prior: 'B',
98
+ no_history_available: 'D',
99
+ zero_balance: 'E',
100
+ collection: 'G',
101
+ foreclosure_completed: 'H',
102
+ voluntary_surrender: 'J',
103
+ repossession: 'K',
104
+ charge_off: 'L'
105
+ }.freeze
106
+
107
+ CONSUMER_TRANSACTION_TYPE = {
108
+ new_account_or_new_borrower: '1',
109
+ name_change: '2',
110
+ address_change: '3',
111
+ ssn_change: '5',
112
+ name_and_address_change: '6',
113
+ name_and_ssn_change: '8',
114
+ address_and_ssn_change: '9',
115
+ name_address_and_ssn_change: 'A'
116
+ }.freeze
117
+
118
+ ADDRESS_INDICATOR = {
119
+ confirmed: 'C',
120
+ known: 'Y',
121
+ not_confirmed: 'N',
122
+ military: 'M',
123
+ secondary: 'S',
124
+ business: 'B',
125
+ non_deliverable: 'U',
126
+ data_reporters_default: 'D',
127
+ bill_payer_service: 'P'
128
+ }.freeze
129
+
130
+ RESIDENCE_CODE = {
131
+ owns: 'O',
132
+ rents: 'R'
133
+ }.freeze
134
+
135
+ GENERATION_CODE = {
136
+ junior: 'J',
137
+ senior: 'S',
138
+ ii: '2',
139
+ iii: '3',
140
+ iv: '4',
141
+ v: '5',
142
+ vi: '6',
143
+ vii: '7',
144
+ viii: '8',
145
+ ix: '9'
146
+ }.freeze
147
+
148
+ CONSUMER_INFORMATION_INDICATOR = {
149
+ petition_ch7: 'A',
150
+ petition_ch11: 'B',
151
+ petition_ch12: 'C',
152
+ petition_ch13: 'D',
153
+ discharged_ch7: 'E',
154
+ discharged_ch11: 'F',
155
+ discharged_ch12: 'G',
156
+ discharged_ch13: 'H',
157
+ dismissed_ch7: 'I',
158
+ dismissed_ch11: 'J',
159
+ dismissed_ch12: 'K',
160
+ dismissed_ch13: 'L',
161
+ withdrawn_ch7: 'M',
162
+ withdrawn_ch11: 'N',
163
+ withdrawn_ch12: 'O',
164
+ withdrawn_ch13: 'P',
165
+ }
166
+
167
+ # K2 Segment constants
168
+ PURCHASED_FROM_SOLD_TO_INDICATOR = {
169
+ purchased_from: 1,
170
+ sold_to: 2,
171
+ remove_previous: 9
172
+ }.freeze
173
+
174
+ ALPHANUMERIC = /\A([[:alnum:]]|\s)+\z/
175
+ ALPHANUMERIC_PLUS_DASH = /\A([[:alnum:]]|\s|\-)+\z/
176
+ ALPHANUMERIC_PLUS_DOT_DASH_SLASH = /\A([[:alnum:]]|\s|\-|\.|\\|\/)+\z/
177
+ NUMERIC = /\A\d+\.?\d*\z/
178
+
179
+ FIXED_LENGTH = 426
180
+
181
+ def self.account_status_needs_payment_rating?(account_status)
182
+ account_status.in?([ACCOUNT_STATUS[:account_transferred], ACCOUNT_STATUS[:closed],
183
+ ACCOUNT_STATUS[:paid_in_full_foreclosure], ACCOUNT_STATUS[:govt_insurance_claim_filed],
184
+ ACCOUNT_STATUS[:deed_received], ACCOUNT_STATUS[:foreclosure_completed],
185
+ ACCOUNT_STATUS[:voluntary_surrender]])
186
+ end
187
+
188
+ def self.alphanumeric_to_metro2(val, required_length, permitted_chars, name)
189
+ # Left justified and blank-filled
190
+ val = val.to_s
191
+
192
+ return ' ' * required_length if val.empty?
193
+
194
+ unless !!(val =~ permitted_chars)
195
+ raise ArgumentError.new("Content (#{val}) contains invalid characters in field '#{name}'")
196
+ end
197
+
198
+ if val.size > required_length
199
+ val[0..(required_length-1)]
200
+ else
201
+ val + (' ' * (required_length - val.size))
202
+ end
203
+ end
204
+
205
+ def self.numeric_to_metro2(val, required_length,
206
+ is_monetary: false, name: nil, possible_values: nil)
207
+ unless possible_values.nil? || possible_values.include?(val)
208
+ raise ArgumentError.new("field #{name} has unsupported value: #{val}")
209
+ end
210
+
211
+ # Right justified and zero-filled
212
+ val = val.to_s
213
+
214
+ return '0' * required_length if val.empty?
215
+
216
+ unless !!(val =~ Metro2::NUMERIC)
217
+ raise ArgumentError.new("field (#{val}) must be numeric")
218
+ end
219
+
220
+ decimal_index = val.index('.')
221
+ val = val[0..(decimal_index - 1)] if decimal_index
222
+
223
+ # any value above 1 billion gets set to 999,999,999
224
+ return '9' * required_length if is_monetary && val.to_f >= 1000000000
225
+ if val.size > required_length
226
+ raise ArgumentError.new("numeric field (#{val}) is too long (max #{required_length})")
227
+ end
228
+
229
+ ('0' * (required_length - val.size)) + val
230
+ end
231
+ end
232
+
233
+ require 'metro_2/fields'
234
+ require 'metro_2/metro2_file'
235
+
236
+ # Require records files
237
+ require 'metro_2/records/record'
238
+
239
+ Dir.new(File.dirname(__FILE__) + '/metro_2/records').each do |file|
240
+ require('metro_2/records/' + File.basename(file)) if File.extname(file) == ".rb"
241
+ end
@@ -0,0 +1,123 @@
1
+ module Metro2
2
+ module Fields
3
+ def alphanumeric_field(name, required_length, permitted_chars = Metro2::ALPHANUMERIC)
4
+ fields << name
5
+
6
+ # getter
7
+ define_method name do
8
+ instance_variable_get("@#{name}")
9
+ end
10
+
11
+ # setter (includes validations)
12
+ define_method "#{name}=" do | val |
13
+ instance_variable_set("@#{name}", val)
14
+ end
15
+
16
+ # to_metro2
17
+ define_method "#{name}_to_metro2" do
18
+ val = instance_variable_get("@#{name}")
19
+ Metro2.alphanumeric_to_metro2(val, required_length, permitted_chars, name)
20
+ end
21
+ end
22
+
23
+ def numeric_field(name, required_length, is_monetary: false, possible_values: nil)
24
+ fields << name
25
+
26
+ # getter
27
+ define_method name do
28
+ instance_variable_get("@#{name}")
29
+ end
30
+
31
+ # setter (includes validations)
32
+ define_method "#{name}=" do | val |
33
+ instance_variable_set("@#{name}", val)
34
+ end
35
+
36
+ # to_metro2
37
+ define_method "#{name}_to_metro2" do
38
+ val = instance_variable_get("@#{name}")
39
+ Metro2.numeric_to_metro2(val, required_length, is_monetary: is_monetary,
40
+ name: name, possible_values: possible_values)
41
+ end
42
+ end
43
+
44
+ def alphanumeric_const_field(name, required_length, val, permitted_chars = Metro2::ALPHANUMERIC)
45
+ fields << name
46
+
47
+ # getter
48
+ define_method name do
49
+ val
50
+ end
51
+
52
+ # to_metro2
53
+
54
+ define_method "#{name}_to_metro2" do
55
+ Metro2.alphanumeric_to_metro2(val, required_length, permitted_chars, name)
56
+ end
57
+ end
58
+
59
+ def numeric_const_field(name, required_length, val, is_monetary = false)
60
+ fields << name
61
+
62
+ # getter
63
+ define_method name do
64
+ val
65
+ end
66
+
67
+ # to_metro2
68
+ define_method "#{name}_to_metro2" do
69
+ Metro2.numeric_to_metro2(val, required_length, is_monetary: is_monetary)
70
+ end
71
+ end
72
+
73
+ def monetary_field(name)
74
+ numeric_field(name, 9, is_monetary: true)
75
+ end
76
+
77
+ def date_field(name)
78
+ fields << name
79
+
80
+ # getter
81
+ define_method name do
82
+ instance_variable_get("@#{name}")
83
+ end
84
+
85
+ # setter (includes validations)
86
+ define_method "#{name}=" do | val |
87
+ instance_variable_set("@#{name}", val)
88
+ end
89
+
90
+ # to_metro2
91
+ define_method "#{name}_to_metro2" do
92
+ # Right justified and zero-filled
93
+ val = instance_variable_get("@#{name}")
94
+ val = val&.strftime('%m%d%Y')
95
+
96
+ Metro2.numeric_to_metro2(val, 8)
97
+ end
98
+ end
99
+
100
+ def timestamp_field(name)
101
+ fields << name
102
+
103
+ # getter
104
+ define_method name do
105
+ instance_variable_get("@#{name}")
106
+ end
107
+
108
+ # setter (includes validations)
109
+ define_method "#{name}=" do | val |
110
+ instance_variable_set("@#{name}", val)
111
+ end
112
+
113
+ # to_metro2
114
+ define_method "#{name}_to_metro2" do
115
+ # Right justified and zero-filled
116
+ val = instance_variable_get("@#{name}")
117
+ val = val&.strftime('%m%d%Y%H%M%S')
118
+ Metro2.numeric_to_metro2(val, 14)
119
+ end
120
+ end
121
+ end
122
+
123
+ end