mt940_parser 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.specification ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mt940
3
+ version: !ruby/object:Gem::Version
4
+ hash: 23
5
+ segments:
6
+ - 1
7
+ - 0
8
+ - 0
9
+ version: 1.0.0
10
+ platform: ruby
11
+ authors: []
12
+
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-03-24 00:00:00 +01:00
18
+ default_executable:
19
+ dependencies: []
20
+
21
+ description:
22
+ email:
23
+ executables: []
24
+
25
+ extensions: []
26
+
27
+ extra_rdoc_files: []
28
+
29
+ files:
30
+ - lib
31
+ - lib/mt940.rb
32
+ has_rdoc: true
33
+ homepage:
34
+ licenses: []
35
+
36
+ post_install_message:
37
+ rdoc_options: []
38
+
39
+ require_paths:
40
+ - lib
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ hash: 3
46
+ segments:
47
+ - 0
48
+ version: "0"
49
+ version:
50
+ required_rubygems_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ hash: 3
55
+ segments:
56
+ - 0
57
+ version: "0"
58
+ version:
59
+ requirements: []
60
+
61
+ rubyforge_project:
62
+ rubygems_version: 1.3.7
63
+ signing_key:
64
+ specification_version: 3
65
+ summary:
66
+ test_files: []
67
+
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+
6
+ # Add dependencies to develop your gem here.
7
+ # Include everything needed to run rake, tests, features, etc.
8
+ group :development do
9
+ gem "rspec", "~> 2.3.0"
10
+ gem "bundler", "~> 1.0.0"
11
+ gem "jeweler", "~> 1.5.2"
12
+ gem "rcov", ">= 0"
13
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,28 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ diff-lcs (1.1.2)
5
+ git (1.2.5)
6
+ jeweler (1.5.2)
7
+ bundler (~> 1.0.0)
8
+ git (>= 1.2.5)
9
+ rake
10
+ rake (0.8.7)
11
+ rcov (0.9.9)
12
+ rspec (2.3.0)
13
+ rspec-core (~> 2.3.0)
14
+ rspec-expectations (~> 2.3.0)
15
+ rspec-mocks (~> 2.3.0)
16
+ rspec-core (2.3.1)
17
+ rspec-expectations (2.3.0)
18
+ diff-lcs (~> 1.1.2)
19
+ rspec-mocks (2.3.0)
20
+
21
+ PLATFORMS
22
+ ruby
23
+
24
+ DEPENDENCIES
25
+ bundler (~> 1.0.0)
26
+ jeweler (~> 1.5.2)
27
+ rcov
28
+ rspec (~> 2.3.0)
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 betterplace
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,20 @@
1
+ = mt940
2
+
3
+ This is a library to parse account statements which are formatted as SWIFT mt940.
4
+
5
+ See the following link for documentation of the format.
6
+ http://www.google.de/url?sa=t&source=web&cd=1&ved=0CBwQFjAA&url=http%3A%2F%2Fmartin.hinner.info%2Fbankconvert%2Fswift_mt940_942.pdf&ei=alO_TObpPNGRswbD15WqDQ&usg=AFQjCNHkVGOfzofgKkk6dEkynAho02S2cg
7
+
8
+ == Note on Patches/Pull Requests
9
+
10
+ * Fork the project.
11
+ * Make your feature addition or bug fix.
12
+ * Add tests for it. This is important so I don't break it in a
13
+ future version unintentionally.
14
+ * Commit, do not mess with rakefile, version, or history.
15
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
16
+ * Send me a pull request. Bonus points for topic branches.
17
+
18
+ == Copyright
19
+
20
+ Copyright (c) 2010 Thies C. Arntzen. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,58 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'rake'
11
+
12
+ begin
13
+ require 'jeweler'
14
+ Jeweler::Tasks.new do |gem|
15
+ gem.name = "mt940_parser"
16
+ gem.summary = %Q{MT940 parses account statements in the SWIFT MT940 format.}
17
+ gem.license = "MIT"
18
+ gem.email = "developers@betterplace.org"
19
+ gem.homepage = "http://github.com/betterplace/mt940_parser"
20
+ gem.authors = ["Thies C. Arntzen", "Phillip Oertel"]
21
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
22
+ end
23
+ Jeweler::GemcutterTasks.new
24
+ rescue LoadError
25
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
26
+ end
27
+
28
+ require 'rake/testtask'
29
+ Rake::TestTask.new(:test) do |test|
30
+ test.libs << 'lib' << 'test'
31
+ test.pattern = 'test/**/test_*.rb'
32
+ test.verbose = true
33
+ end
34
+
35
+ begin
36
+ require 'rcov/rcovtask'
37
+ Rcov::RcovTask.new do |test|
38
+ test.libs << 'test'
39
+ test.pattern = 'test/**/test_*.rb'
40
+ test.verbose = true
41
+ end
42
+ rescue LoadError
43
+ task :rcov do
44
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
45
+ end
46
+ end
47
+
48
+ task :default => :test
49
+
50
+ require 'rake/rdoctask'
51
+ Rake::RDocTask.new do |rdoc|
52
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
53
+
54
+ rdoc.rdoc_dir = 'rdoc'
55
+ rdoc.title = "mt940 #{version}"
56
+ rdoc.rdoc_files.include('README*')
57
+ rdoc.rdoc_files.include('lib/**/*.rb')
58
+ end
data/VERSION.yml ADDED
@@ -0,0 +1,5 @@
1
+ ---
2
+ :patch: 0
3
+ :build:
4
+ :major: 1
5
+ :minor: 0
Binary file
Binary file
data/docs/mt940.pdf ADDED
Binary file
Binary file
@@ -0,0 +1,73 @@
1
+ # this is a beautification wrapper around the low-level
2
+ # MT940.parse command. use it in order to make dealing with
3
+ # the data easier
4
+ class MT940
5
+
6
+ class CustomerStatementMessage
7
+
8
+ attr_reader :statement_lines
9
+
10
+ def self.parse_file(file)
11
+ self.parse(File.read(file))
12
+ end
13
+
14
+ def self.parse(data)
15
+ messages = MT940.parse(data)
16
+ messages.map { |msg| new(msg) }
17
+ end
18
+
19
+ def initialize(raw_mt940)
20
+ @raw = raw_mt940
21
+ @account = @raw.find { |line| line.class == MT940::Account }
22
+ @statement_lines = []
23
+ @raw.each_with_index do |line, i|
24
+ next unless line.class == MT940::StatementLine
25
+ ensure_is_info_line!(@raw[i+1])
26
+ @statement_lines << StatementLineBundle.new(@raw[i], @raw[i+1])
27
+ end
28
+ end
29
+
30
+ def bank_code
31
+ @account.bank_code
32
+ end
33
+
34
+ def account_number
35
+ @account.account_number
36
+ end
37
+
38
+ private
39
+
40
+ def ensure_is_info_line!(line)
41
+ unless line.is_a?(MT940::StatementLineInformation)
42
+ raise StandardError, "Unexpected Structure; expected StatementLineInformation, but was #{line.class}"
43
+ end
44
+ end
45
+
46
+ end
47
+
48
+ class StatementLineBundle
49
+
50
+ METHOD_MAP = {
51
+ :amount => :line,
52
+ :funds_code => :line,
53
+ :value_date => :line,
54
+ :entry_date => :line,
55
+ :account_holder => :info,
56
+ :details => :info,
57
+ :account_number => :info,
58
+ :bank_code => :info,
59
+ }
60
+
61
+ def initialize(statement_line, statement_line_info)
62
+ @line, @info = statement_line, statement_line_info
63
+ end
64
+
65
+ def method_missing(method, *args, &block)
66
+ super unless METHOD_MAP.has_key?(method)
67
+ object = instance_variable_get("@#{METHOD_MAP[method.to_sym]}")
68
+ object.send(method)
69
+ end
70
+
71
+ end
72
+
73
+ end
data/lib/mt940.rb ADDED
@@ -0,0 +1,237 @@
1
+ class MT940
2
+ class Field
3
+ attr_reader :modifier, :content
4
+
5
+ DATE = /(\d{2})(\d{2})(\d{2})/
6
+ SHORT_DATE = /(\d{2})(\d{2})/
7
+
8
+ class << self
9
+
10
+ def for(line)
11
+ if line.match(/^:(\d{2,2})(\w)?:(.*)$/)
12
+ number, modifier, content = $1, $2, $3
13
+ klass = {
14
+ '20' => Job,
15
+ '21' => Reference,
16
+ '25' => Account,
17
+ '28' => Statement,
18
+ '60' => AccountBalance,
19
+ '61' => StatementLine,
20
+ '62' => ClosingBalance,
21
+ '64' => ValutaBalance,
22
+ '65' => FutureValutaBalance,
23
+ '86' => StatementLineInformation
24
+ }[number]
25
+
26
+ raise StandardError, "Field #{number} is not implemented" unless klass
27
+
28
+ klass.new(modifier, content)
29
+ else
30
+ raise StandardError, "Wrong line format: #{line.dump}"
31
+ end
32
+ end
33
+ end
34
+
35
+ def initialize(modifier, content)
36
+ @modifier = modifier
37
+ parse_content(content)
38
+ end
39
+
40
+ private
41
+ def parse_amount_in_cents(amount)
42
+ # don't use Integer(amount) function, because amount can be "008" - interpreted as octal number ("010" = 8)
43
+ amount.gsub(',', '').to_i
44
+ end
45
+
46
+ def parse_date(date)
47
+ date.match(DATE)
48
+ Date.new("20#{$1}".to_i, $2.to_i, $3.to_i)
49
+ end
50
+
51
+ def parse_entry_date(raw_entry_date, value_date)
52
+ raw_entry_date.match(SHORT_DATE)
53
+ entry_date = Date.new(value_date.year, $1.to_i, $2.to_i)
54
+ if (entry_date.year != value_date.year)
55
+ raise "Unhandled case: value date and entry date are in different years"
56
+ end
57
+ entry_date
58
+ end
59
+ end
60
+
61
+ # 20
62
+ class Job < Field
63
+ attr_reader :reference
64
+
65
+ def parse_content(content)
66
+ @reference = content
67
+ end
68
+ end
69
+
70
+ # 21
71
+ class Reference < Job
72
+ end
73
+
74
+ # 25
75
+ class Account < Field
76
+ attr_reader :bank_code, :account_number, :account_currency
77
+
78
+ def parse_content(content)
79
+ content.match(/^(.{8,11})\/(\d{0,23})([A-Z]{3})?$/)
80
+ @bank_code, @account_number, @account_currency = $1, $2, $3
81
+ end
82
+ end
83
+
84
+ # 28
85
+ class Statement < Field
86
+ attr_reader :number, :sheet
87
+
88
+ def parse_content(content)
89
+ content.match(/^(0|(\d{5,5})\/(\d{2,5}))$/)
90
+ if $1 == '0'
91
+ @number = @sheet = 0
92
+ else
93
+ @number, @sheet = $2.to_i, $3.to_i
94
+ end
95
+ end
96
+ end
97
+
98
+ # 60
99
+ class AccountBalance < Field
100
+ attr_reader :balance_type, :sign, :currency, :amount, :date
101
+
102
+ def parse_content(content)
103
+ content.match(/^(C|D)(\w{6})(\w{3})(\d{1,12},\d{0,2})$/)
104
+
105
+ @balance_type = case @modifier
106
+ when 'F'
107
+ :start
108
+ when 'M'
109
+ :intermediate
110
+ end
111
+
112
+ @sign = case $1
113
+ when 'C'
114
+ :credit
115
+ when 'D'
116
+ :debit
117
+ end
118
+
119
+ raw_date = $2
120
+ @currency = $3
121
+ @amount = parse_amount_in_cents($4)
122
+
123
+ @date = case raw_date
124
+ when 'ALT', '0'
125
+ nil
126
+ when DATE
127
+ Date.new("20#{$1}".to_i, $2.to_i, $3.to_i)
128
+ end
129
+ end
130
+ end
131
+
132
+ # 61
133
+ class StatementLine < Field
134
+ attr_reader :date, :entry_date, :funds_code, :amount, :swift_code, :reference, :transaction_description
135
+
136
+ def parse_content(content)
137
+ content.match(/^(\d{6})(\d{4})?(C|D|RC|RD)\D?(\d{1,12},\d{0,2})((?:N|F).{3})(NONREF|.{0,16})(?:$|\/\/)(.*)/).to_a
138
+
139
+ raw_date = $1
140
+ raw_entry_date = $2
141
+ @funds_code = case $3
142
+ when 'C'
143
+ :credit
144
+ when 'D'
145
+ :debit
146
+ when 'RC'
147
+ :return_credit
148
+ when 'RD'
149
+ :return_debit
150
+ end
151
+
152
+ @amount = parse_amount_in_cents($4)
153
+ @swift_code = $5
154
+ @reference = $6
155
+ @transaction_description = $7
156
+
157
+ @date = parse_date(raw_date)
158
+ @entry_date = parse_entry_date(raw_entry_date, @date)
159
+ end
160
+
161
+ def value_date
162
+ @date
163
+ end
164
+ end
165
+
166
+ # 62
167
+ class ClosingBalance < AccountBalance
168
+ end
169
+
170
+ # 64
171
+ class ValutaBalance < AccountBalance
172
+ end
173
+
174
+ # 65
175
+ class FutureValutaBalance < AccountBalance
176
+ end
177
+
178
+ # 86
179
+ class StatementLineInformation < Field
180
+ attr_reader :code, :transaction_description, :prima_nota, :details, :bank_code, :account_number,
181
+ :account_holder, :text_key_extension, :not_implemented_fields
182
+
183
+ def parse_content(content)
184
+ content.match(/^(\d{3})((.).*)$/)
185
+ @code = $1.to_i
186
+
187
+ details = []
188
+ account_holder = []
189
+
190
+ if seperator = $3
191
+ sub_fields = $2.scan(/#{Regexp.escape(seperator)}(\d{2})([^#{Regexp.escape(seperator)}]*)/)
192
+
193
+
194
+ sub_fields.each do |(code, content)|
195
+ case code.to_i
196
+ when 0
197
+ @transaction_description = content
198
+ when 10
199
+ @prima_nota = content
200
+ when 20..29, 60..63
201
+ details << content
202
+ when 30
203
+ @bank_code = content
204
+ when 31
205
+ @account_number = content
206
+ when 32..33
207
+ account_holder << content
208
+ when 34
209
+ @text_key_extension = content
210
+ else
211
+ @not_implemented_fields ||= []
212
+ @not_implemented_fields << [code, content]
213
+ $stderr << "code not implemented: code:#{code} content:»#{content}«\n" if $DEBUG
214
+ end
215
+ end
216
+ end
217
+
218
+ @details = details.join("\n")
219
+ @account_holder = account_holder.join("\n")
220
+ end
221
+ end
222
+
223
+ class << self
224
+ def parse(text)
225
+ text << "\r\n" if text[-1,1] == '-'
226
+ raw_sheets = text.split(/^-\r\n/).map { |sheet| sheet.gsub(/\r\n(?!:)/, '') }
227
+ sheets = raw_sheets.map { |raw_sheet| parse_sheet(raw_sheet) }
228
+ end
229
+
230
+ private
231
+ def parse_sheet(sheet)
232
+ lines = sheet.split("\r\n")
233
+ fields = lines.reject { |line| line.empty? }.map { |line| Field.for(line) }
234
+ fields
235
+ end
236
+ end
237
+ end
@@ -0,0 +1 @@
1
+ :25:51210600/9223382012EUR
@@ -0,0 +1,6 @@
1
+ ---
2
+ - - !ruby/object:MT940::Account
3
+ account_currency: EUR
4
+ account_number: "9223382012"
5
+ bank_code: "51210600"
6
+ modifier:
@@ -0,0 +1,2 @@
1
+ :86:091
2
+ -
@@ -0,0 +1,6 @@
1
+ ---
2
+ - - !ruby/object:MT940::StatementLineInformation
3
+ account_holder: ""
4
+ code: 0
5
+ details: ""
6
+ modifier:
@@ -0,0 +1,2 @@
1
+
2
+ :20:TELEREPORTING
@@ -0,0 +1,4 @@
1
+ ---
2
+ - - !ruby/object:MT940::Job
3
+ modifier:
4
+ reference: TELEREPORTING
@@ -0,0 +1,2 @@
1
+ :62F:C100323EUR42570,04
2
+ -
@@ -0,0 +1,8 @@
1
+ ---
2
+ - - !ruby/object:MT940::ClosingBalance
3
+ amount: 4257004
4
+ balance_type: :start
5
+ currency: EUR
6
+ date: 2010-03-23
7
+ modifier: F
8
+ sign: :credit