cmxl 0.0.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 (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +136 -0
  6. data/Rakefile +7 -0
  7. data/cmxl.gemspec +24 -0
  8. data/lib/cmxl.rb +20 -0
  9. data/lib/cmxl/field.rb +78 -0
  10. data/lib/cmxl/fields/account_balance.rb +45 -0
  11. data/lib/cmxl/fields/account_identification.rb +12 -0
  12. data/lib/cmxl/fields/available_balance.rb +8 -0
  13. data/lib/cmxl/fields/closing_balance.rb +8 -0
  14. data/lib/cmxl/fields/reference.rb +21 -0
  15. data/lib/cmxl/fields/statement_details.rb +54 -0
  16. data/lib/cmxl/fields/statement_line.rb +47 -0
  17. data/lib/cmxl/fields/statement_number.rb +8 -0
  18. data/lib/cmxl/statement.rb +84 -0
  19. data/lib/cmxl/transaction.rb +91 -0
  20. data/lib/cmxl/version.rb +3 -0
  21. data/spec/fields/account_balance_spec.rb +41 -0
  22. data/spec/fields/account_identification_spec.rb +24 -0
  23. data/spec/fields/available_balance_spec.rb +16 -0
  24. data/spec/fields/closing_balance_spec.rb +15 -0
  25. data/spec/fields/reference_spec.rb +12 -0
  26. data/spec/fields/statement_details_spec.rb +40 -0
  27. data/spec/fields/statement_number_spec.rb +10 -0
  28. data/spec/fields/statment_line_spec.rb +19 -0
  29. data/spec/fields/unknown_spec.rb +9 -0
  30. data/spec/fixtures/lines/account_balance_credit.txt +1 -0
  31. data/spec/fixtures/lines/account_balance_debit.txt +1 -0
  32. data/spec/fixtures/lines/account_identification_iban.txt +1 -0
  33. data/spec/fixtures/lines/account_identification_legacy.txt +1 -0
  34. data/spec/fixtures/lines/available_balance.txt +1 -0
  35. data/spec/fixtures/lines/closing_balance.txt +1 -0
  36. data/spec/fixtures/lines/reference.txt +1 -0
  37. data/spec/fixtures/lines/statement_details.txt +1 -0
  38. data/spec/fixtures/lines/statement_line.txt +1 -0
  39. data/spec/fixtures/lines/statement_number.txt +1 -0
  40. data/spec/fixtures/mt940.txt +75 -0
  41. data/spec/mt940_parsing_spec.rb +44 -0
  42. data/spec/spec_helper.rb +30 -0
  43. data/spec/support/fixtures.rb +7 -0
  44. data/spec/transaction_spec.rb +28 -0
  45. metadata +155 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 471c7b0121f9cd95bafcfe16586d35b9cd9ec50a
4
+ data.tar.gz: 5694a0e0d8089bc656aae5baa3c923277e7ad8ed
5
+ SHA512:
6
+ metadata.gz: 9a58700e14cda9fca9b787e70970630324800606f4e5652f588bcb1fa5f9d9f9f8e7f91269d98e7d2d5be67f002be64bb2fc54cfb06f47151f71df0640185157
7
+ data.tar.gz: a3e652b2dec634c5b1bfe9292a65ae83f72a18a0402ca7d3c8ca8b253d4b730f0ceb2367a22779f0968769e6720cb744e96de4a6a9f7147064da35b7fda639ae
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in cmxl.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Michael Bumann
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,136 @@
1
+ # Cmxl - your friendly ruby MT940 parser
2
+
3
+ At Railslove we build a lot of banking and payment applications and work on integrating applications with banks and banking functionality.
4
+ Our goal is to making it easy with what sometimes seems complicated.
5
+
6
+ Cmxl is a friendly and extendible MT940 bank statement file parser that helps your extracting data from the bank statement files.
7
+
8
+ ## What is MT940?
9
+
10
+ MT940 (MT = Message Type) is the SWIFT-Standard for the electronic transfer of bank statement files.
11
+ When integrating with banks you often get MT940 files as interface.
12
+ For more information have a look at the different [SWIFT message types](http://en.wikipedia.org/wiki/SWIFT_message_types)
13
+
14
+ At some point in the future MT940 file should be exchanged with newer XML documents - but banking institutions are slow so MT940 will stick around for a while.
15
+
16
+ ## Reqirements
17
+
18
+ Cmxl is a pure ruby parser and has no gem dependencies.
19
+
20
+ * Ruby 1.9.3 or newer
21
+
22
+ ## Installation
23
+
24
+
25
+ Add this line to your application's Gemfile:
26
+
27
+ gem 'cmxl'
28
+
29
+ And then execute:
30
+
31
+ $ bundle
32
+
33
+ Or install it yourself as:
34
+
35
+ $ gem install cmxl
36
+
37
+ ## Usage
38
+
39
+ Simple usage:
40
+
41
+ ```ruby
42
+
43
+ statements = Cmxl.parse(File.read('mt940.txt')) # parses the file and returns an array of statement objects
44
+ statements.each do |s|
45
+ puts s.reference
46
+ puts s.generation_date
47
+ puts s.opening_balance.amount
48
+ puts s.closing_balance.amount
49
+ puts.sha # SHA of the statement source - could be used as an unique identifier
50
+
51
+ s.transactions.each do |t|
52
+ puts t.information
53
+ puts t.description
54
+ puts t.entry_date
55
+ puts t.funds_code
56
+ puts t.credit?
57
+ puts t.debit?
58
+ puts t.sign # -1 if it's a debit; 1 if it's a credit
59
+ puts t.name
60
+ puts t.iban
61
+ puts t.sepa
62
+ puts t.sub_fields
63
+ puts t.reference
64
+ puts t.bank_reference
65
+ # ...
66
+ end
67
+ end
68
+
69
+ ```
70
+
71
+ Every object responds to `to_h` and let's you easily convert the data to a hash.
72
+
73
+ #### A note about encoding and file wirednesses
74
+
75
+ You probably will encounter encoding issues (hey, you are building banking applications!).
76
+ We try to handle encoding and format wirednesses as much as possible.
77
+ If you have encoding issues you can pass encoding options to the `Cmxl.parse(<string>, <options hash>)` it accepts the same options as [String#encode](http://ruby-doc.org/core-2.1.3/String.html#method-i-encode)
78
+ If that fails try to motify the file before you pass it to the parser - and please create an issue.
79
+
80
+ ### Custom field parsers
81
+
82
+ Because a lot of banks implement the MT940 format slightly different one of the design goals of this library is to be able to customize the field parsers.
83
+ Every line get parsed with a special parser. Here is how to write your own parser:
84
+
85
+ ```ruby
86
+
87
+ # simply create a new parser class inheriting from Cmxl::Field
88
+ class MyFieldParser < Cmxl::Field
89
+ self.tag = 42 # define which MT940 tag your parser can handle. This will automatically register your parser and overwriting existing parsers
90
+ self.parser = /(?<world>.*)/ # the regex to parse the line. Use named regexp to access your match.
91
+
92
+ def upcased
93
+ self.data['world'].upcase
94
+ end
95
+ end
96
+
97
+ my_field_parser = MyFieldParser.parse(":42:hello from mt940")
98
+ my_field_parser.world #=> hello from MT940
99
+ my_field_parser.upcased #=> HELLO FROM MT940
100
+ my_field_parser.data #=> {'world' => 'hello from mt940'} - data is the accessor to the regexp matches
101
+
102
+ ```
103
+
104
+ ## Parsing issues? - please create an issue with your file
105
+
106
+ The Mt940 format often looks different for the different banks and the different countries. Especially the not strict defined fields are often used for custom bank data.
107
+ If you have a file that can not be parsed please open an issue. We hope to build a parser that handles most of the files.
108
+
109
+
110
+ ## ToDo
111
+
112
+ * collect MT940 files from different banks and use them as example for specs
113
+
114
+
115
+ ## Contributing
116
+
117
+ ### Specs
118
+ We use rspec to test Cmxl. Run `rake` to execute the whole test suite.
119
+
120
+ 1. Fork it ( http://github.com/railslove/cmxl/fork )
121
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
122
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
123
+ 4. Push to the branch (`git push origin my-new-feature`)
124
+ 5. Create new Pull Request
125
+
126
+ ## Credits and other parsers
127
+
128
+ Cmxl is inspired and borrows ideas from the `mt940_parser` by the great people at [betterplace](https://www.betterplace.org/).
129
+
130
+ other parsers:
131
+ * [betterplace/mt940_parser](https://github.com/betterplace/mt940_parser)
132
+ * [gmitrev/mt940parser](https://github.com/gmitrev/mt940parser)
133
+
134
+ ------------
135
+
136
+ 2014 - built with love by [Railslove](http://railslove.com). We have built quite a number of FinTech products. If you need support we are happy to help. Please contact us at team@railslove.com.
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rspec/core/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ task default: :spec
data/cmxl.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'cmxl/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "cmxl"
8
+ spec.version = Cmxl::VERSION
9
+ spec.authors = ["Michael Bumann"]
10
+ spec.email = ["michael@railslove.com"]
11
+ spec.summary = %q{Cmxl is your friendly MT940 bank statement parser}
12
+ spec.description = %q{Cmxl provides an extensible and customizable parser for the MT940 bank statement format.}
13
+ spec.homepage = "http://railslove.com"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.5"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency "rspec", '~>3.0'
24
+ end
data/lib/cmxl.rb ADDED
@@ -0,0 +1,20 @@
1
+ require "cmxl/version"
2
+
3
+ require 'cmxl/field'
4
+ require 'cmxl/statement'
5
+ require 'cmxl/transaction'
6
+ Dir[File.join(File.dirname(__FILE__), 'cmxl/fields', '*.rb')].each { |f| require f; }
7
+ module Cmxl
8
+
9
+ STATEMENT_SEPARATOR = "\n-"
10
+ def self.parse(data, options={})
11
+ options[:universal_newline] ||= true
12
+ if options[:encoding]
13
+ data.encode!('UTF-8', options.delete(:encoding), options)
14
+ else
15
+ data.encode!('UTF-8', options) if !options.empty?
16
+ end
17
+
18
+ data.split(STATEMENT_SEPARATOR).reject { |s| s.strip.empty? }.collect {|s| Cmxl::Statement.new(s.strip) }
19
+ end
20
+ end
data/lib/cmxl/field.rb ADDED
@@ -0,0 +1,78 @@
1
+ require 'date'
2
+ module Cmxl
3
+ class Field
4
+ class LineFormatError < StandardError; end
5
+ class Unknown < Field;
6
+ @parser= /(?<source>.*)/
7
+ def to_h
8
+ { tag: tag, modifier: modifier, source: source }
9
+ end
10
+ end
11
+ DATE = /(?<year>\d{0,2})(?<month>\d{2})(?<day>\d{2})/
12
+ attr_accessor :source, :modifier, :match, :data, :tag
13
+
14
+ @@parsers = {}
15
+ @@parsers.default = Unknown
16
+ def self.parsers; @@parsers; end
17
+
18
+ class << self; attr_accessor :parser; end
19
+ def self.tag=(tag)
20
+ @tag = tag.to_s
21
+ @@parsers[tag.to_s] = self
22
+ end
23
+ def self.tag
24
+ @tag
25
+ end
26
+
27
+ def self.parse(line)
28
+ if line.match(/^:(\d{2,2})(\w)?:(.*)$/)
29
+ tag, modifier, content = $1, $2, $3
30
+ Field.parsers[tag.to_s].new(content, modifier, tag)
31
+ else
32
+ raise LineFormatError, "Wrong line format: #{line.dump}"
33
+ end
34
+ end
35
+
36
+ def initialize(source, modifier=nil, tag=nil)
37
+ self.tag = tag
38
+ self.modifier = modifier
39
+ self.source = source
40
+ self.data = {}
41
+
42
+ if self.match = self.source.match(self.class.parser)
43
+ self.match.names.each do |name|
44
+ self.data[name] = self.match[name]
45
+ end
46
+ end
47
+ end
48
+
49
+ def to_h
50
+ self.data
51
+ end
52
+ alias :to_hash :to_h
53
+ def to_json(*args)
54
+ to_h.to_json(*args)
55
+ end
56
+
57
+ def to_date(date, year=nil)
58
+ if match = date.match(DATE)
59
+ year ||= "20#{match['year']}"
60
+ month = match['month']
61
+ day = match['day']
62
+ Date.new(year.to_i, month.to_i, day.to_i)
63
+ else
64
+ date
65
+ end
66
+ rescue ArgumentError # let's simply ignore invalid dates
67
+ date
68
+ end
69
+
70
+ def method_missing(m, *value)
71
+ if m =~ /=\z/
72
+ self.data[m] = value.first
73
+ else
74
+ self.data[m.to_s]
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,45 @@
1
+ module Cmxl
2
+ module Fields
3
+ class AccountBalance < Field
4
+ self.tag = 60
5
+ self.parser = /(?<funds_code>\A[a-zA-Z]{1})(?<date>\d{6})(?<currency>[a-zA-Z]{3})(?<amount>[\d|,|\.]{4,15})/i
6
+
7
+ def date
8
+ to_date(self.data['date'])
9
+ end
10
+
11
+ def credit?
12
+ self.data['funds_code'].to_s.upcase == 'C'
13
+ end
14
+
15
+ def debit?
16
+ !credit?
17
+ end
18
+
19
+ def amount
20
+ self.data['amount'].gsub(',','.').to_f * self.sign
21
+ end
22
+
23
+ def sign
24
+ self.credit? ? 1 : -1
25
+ end
26
+
27
+ def amount_in_cents
28
+ self.data['amount'].gsub(',', '').gsub('.','').to_i * self.sign
29
+ end
30
+
31
+ def to_h
32
+ {
33
+ 'date' => date,
34
+ 'funds_code' => funds_code,
35
+ 'credit' => credit?,
36
+ 'debit' => debit?,
37
+ 'currency' => currency,
38
+ 'amount' => amount,
39
+ 'amount_in_cents' => amount_in_cents,
40
+ 'sign' => sign
41
+ }
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,12 @@
1
+ module Cmxl
2
+ module Fields
3
+ class AccountIdentification < Field
4
+ self.tag = 25
5
+ self.parser = /(?<bank_code>\w{8,11})\/(?<account_number>\d{0,23})(?<currency>[A-Z]{3})?|(?<country>[a-zA-Z]{2})(?<ban>\d{11,36})/i
6
+
7
+ def iban
8
+ "#{self.country}#{self.ban}"
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,8 @@
1
+ module Cmxl
2
+ module Fields
3
+ class AvailableBalance < AccountBalance
4
+ self.tag = 64
5
+ self.parser = superclass.parser
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ module Cmxl
2
+ module Fields
3
+ class ClosingBalance < AccountBalance
4
+ self.tag = 62
5
+ self.parser = superclass.parser
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,21 @@
1
+ module Cmxl
2
+ module Fields
3
+ class Reference < Field
4
+ self.tag = 20
5
+ self.parser = /(?<statement_identifier>[a-zA-Z]{0,2})(?<date>\d{6})(?<additional_number>.*)/i
6
+
7
+ def reference
8
+ self.source
9
+ end
10
+
11
+ def date
12
+ to_date(self.data['date'])
13
+ end
14
+
15
+ def to_h
16
+ super.merge({'date' => date, 'reference' => source})
17
+ end
18
+
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,54 @@
1
+ module Cmxl
2
+ module Fields
3
+ class StatementDetails < Field
4
+ self.tag = 86
5
+ self.parser = /(?<transaction_code>\w{3})(?<details>(?<seperator>.).*)/
6
+
7
+ def sub_fields
8
+ @sub_fields ||= self.data['details'].split(/#{Regexp.escape(self.data['seperator'])}(\d{2})/).reject(&:empty?).each_slice(2).to_h
9
+ end
10
+
11
+ def description
12
+ self.sub_fields['00']
13
+ end
14
+
15
+ def information
16
+ (20..29).to_a.collect {|i| self.sub_fields[i.to_s] }.join('')
17
+ end
18
+
19
+ def sepa
20
+ if self.information =~ /([A-Z]{4})\+/
21
+ self.information.split(/([A-Z]{4})\+/).reject(&:empty?).each_slice(2).to_h
22
+ else
23
+ {}
24
+ end
25
+ end
26
+
27
+ def bic
28
+ self.sub_fields['30']
29
+ end
30
+
31
+ def name
32
+ "#{self.sub_fields['32']}#{self.sub_fields['33']}"
33
+ end
34
+
35
+ def iban
36
+ self.sub_fields['38'] || self.sub_fields['31']
37
+ end
38
+
39
+ def to_h
40
+ {
41
+ 'bic' => bic,
42
+ 'iban' => iban,
43
+ 'name' => name,
44
+ 'sepa' => sepa,
45
+ 'information' => information,
46
+ 'description' => description,
47
+ 'sub_fields' => sub_fields,
48
+ 'transaction_code' => transaction_code,
49
+ 'details' => details
50
+ }
51
+ end
52
+ end
53
+ end
54
+ end