zilverline-mt940 1.0 → 2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +8 -8
  2. data/Gemfile.lock +1 -1
  3. data/README.md +5 -4
  4. data/lib/mt940.rb +1 -6
  5. data/lib/mt940/version.rb +2 -2
  6. data/lib/mt940_structured/file_content.rb +46 -0
  7. data/lib/mt940_structured/header.rb +30 -0
  8. data/lib/mt940_structured/mt940_structured.rb +8 -0
  9. data/lib/mt940_structured/parser.rb +18 -0
  10. data/lib/mt940_structured/parsers/abnamro/abnamro.rb +5 -0
  11. data/lib/mt940_structured/parsers/abnamro/parser.rb +15 -0
  12. data/lib/mt940_structured/parsers/abnamro/transaction_parser.rb +55 -0
  13. data/lib/mt940_structured/parsers/balance_parser.rb +12 -0
  14. data/lib/mt940_structured/parsers/bank_statement_parser.rb +59 -0
  15. data/lib/mt940_structured/parsers/base.rb +34 -0
  16. data/lib/mt940_structured/parsers/date_parser.rb +7 -0
  17. data/lib/mt940_structured/parsers/default_line61_parser.rb +25 -0
  18. data/lib/mt940_structured/parsers/iban_support.rb +15 -0
  19. data/lib/mt940_structured/parsers/ing/ing.rb +7 -0
  20. data/lib/mt940_structured/parsers/ing/parser.rb +18 -0
  21. data/lib/mt940_structured/parsers/ing/structured_transaction_parser.rb +50 -0
  22. data/lib/mt940_structured/parsers/ing/transaction_parser.rb +31 -0
  23. data/lib/mt940_structured/parsers/ing/types.rb +26 -0
  24. data/lib/mt940_structured/parsers/parsers.rb +16 -0
  25. data/lib/mt940_structured/parsers/rabobank/parser.rb +13 -0
  26. data/lib/mt940_structured/parsers/rabobank/rabobank.rb +8 -0
  27. data/lib/mt940_structured/parsers/rabobank/structured_transaction_parser.rb +41 -0
  28. data/lib/mt940_structured/parsers/rabobank/transaction_parser.rb +29 -0
  29. data/lib/mt940_structured/parsers/rabobank/types.rb +714 -0
  30. data/lib/mt940_structured/parsers/structured_description_parser.rb +12 -0
  31. data/lib/mt940_structured/parsers/tridios/parser.rb +14 -0
  32. data/lib/mt940_structured/parsers/tridios/transaction_parser.rb +23 -0
  33. data/lib/mt940_structured/parsers/tridios/triodos.rb +5 -0
  34. data/spec/fixtures/ing/eu_incasso.txt +17 -0
  35. data/spec/fixtures/ing/eu_incasso_foreign_transaction.txt +17 -0
  36. data/spec/fixtures/ing/failing.txt +18 -0
  37. data/spec/mt940_abnamro_spec.rb +18 -6
  38. data/spec/mt940_ing_spec.rb +78 -2
  39. data/spec/mt940_rabobank_spec.rb +11 -11
  40. data/spec/mt940_structured/file_content_spec.rb +77 -0
  41. data/spec/mt940_structured/header_spec.rb +32 -0
  42. data/spec/mt940_structured/parsers/rabobank/bank_statement_parser_spec.rb +32 -0
  43. data/spec/mt940_triodos_spec.rb +1 -1
  44. data/spec/mt940_two_accounts_spec.rb +1 -1
  45. metadata +41 -9
  46. data/lib/mt940/banks/abnamro.rb +0 -76
  47. data/lib/mt940/banks/ing.rb +0 -84
  48. data/lib/mt940/banks/rabobank.rb +0 -770
  49. data/lib/mt940/banks/triodos.rb +0 -20
  50. data/lib/mt940/base.rb +0 -165
  51. data/lib/mt940/structured_format.rb +0 -16
  52. data/spec/mt940_base_spec.rb +0 -48
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- M2VjYjdlOGZlYzJkMDZmOWIzZTkzODkwNjc0YThmYjg0NTg0MDdmOA==
4
+ MTJlMDQ3YTFiYWIxM2NkODRkMzY0Y2I3MzRmZTRiNDA0YzlhZThiNA==
5
5
  data.tar.gz: !binary |-
6
- Mjg3Mzg5YmYzMTE4NDkzYWIwYTBhMWVkMDlhM2U1M2UyYjg5ZTY1ZQ==
6
+ MjU0N2ZlY2NkYjU2YTFjMGVkZDU4MjhhMzkyYWM4ZGY4YjczNDRiNA==
7
7
  !binary "U0hBNTEy":
8
8
  metadata.gz: !binary |-
9
- YmRlMTM0ZTI5MzdmYmE3NGIwZTBmNGQwOTI1YmU1NzJlY2I1ZDNkNTk0YWY2
10
- NWZhNzYzYWU1ZTFmNDJmNTE0NzRhZTgzN2M1NGIxNTNiN2Q1MjkzMzM5MzVj
11
- ZjRjMDBlYjRiM2Y4M2IzY2Y5M2Q1OWVmYTFmMGQzNTMyMjI3NTM=
9
+ ZWVlNmQ0YmIyYmU0ZDc4M2E0ZWFlMWQzNzY1OTQ1YmI1NjVhNDFkNzJlMDA3
10
+ Y2NkMjAyZDkyNDdjYzM1N2ZjNDY5ZWJlZWU2MmQ1MmQzMjQ4Mjk3ODMwNDIw
11
+ YTQ0ZGNmMGM2YWY2YTVmY2I3MjY5NmVhYzY1MDcxYzRhMDEzOGY=
12
12
  data.tar.gz: !binary |-
13
- NWU5Njk2YWU4N2E1ZTQ0YjUzZDVhZjkzY2ViNTEyOTQ0ZTE5MGQ0MjBkOWNi
14
- NzRmOTNmNGZiODQ0MjZiMDE4ZDE4YzQ3N2M2MDYyYjc2OWY5YjAzNGJlMzUy
15
- YzdmMGFmYzg2NjRiOTAwNDFkODkxODU4NTU2ODJmYjBhNTQzM2M=
13
+ Y2RjMzExNTk2MDA1OTg4MWM0MDIyOTFkZDExNjg5ZmVlNjllZDdmNzQ5Mzc2
14
+ NTA4NTI4ZDExMTljZDI5MTVjYjNiNzgzMjRlMDZmOTJjYzMwMjRkYzI5YjY1
15
+ NTYwZGNkZGY3NDM4OTE0Y2IzZTRjMDdiNDU1NGMxNTNhZTMyYzk=
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- zilverline-mt940 (1.0)
4
+ zilverline-mt940 (2.0)
5
5
 
6
6
  GEM
7
7
  remote: http://rubygems.org/
data/README.md CHANGED
@@ -1,8 +1,9 @@
1
1
  MT940
2
2
  ======
3
3
 
4
- Full parser for MT940 files, see [MT940](http://nl.wikipedia.org/wiki/MT940). This is based on
5
- the original gem of [Frank Oxener - Agile Dovadi BV](http://github.com/dovadi/mt940) but completely redesigned and extended.
4
+ Full parser for MT940 files, see [MT940](http://nl.wikipedia.org/wiki/MT940).
5
+ Initially this is based on the original gem of [Frank Oxener - Agile Dovadi BV](http://github.com/dovadi/mt940)
6
+ but as of version 2.0 completely rewritten in order to support MT940-structured format introduced by SEPA.
6
7
 
7
8
  The following Dutch banks are implemented:
8
9
 
@@ -18,7 +19,7 @@ With the file name as argument:
18
19
 
19
20
  file_name = '~/Downloads/ing.940'
20
21
 
21
- @parse_result = MT940::Base.parse_mt940(file_name)
22
+ @parse_result = MT940Structured::Parser.parse_mt940(file_name)
22
23
 
23
24
  or with the file itself:
24
25
 
@@ -26,7 +27,7 @@ or with the file itself:
26
27
 
27
28
  file = File.open(file_name)
28
29
 
29
- @parse_result = MT940::Base.parse_mt940(file)
30
+ @parse_result = MT940Structured::Parser.parse_mt940(file)
30
31
 
31
32
  after parsing:
32
33
 
@@ -2,9 +2,4 @@ require 'tempfile'
2
2
  require 'date'
3
3
  require_relative 'mt940/transaction'
4
4
  require_relative 'mt940/bank_statement'
5
- require_relative 'mt940/base'
6
- require_relative 'mt940/structured_format'
7
- require_relative 'mt940/banks/ing'
8
- require_relative 'mt940/banks/rabobank'
9
- require_relative 'mt940/banks/abnamro'
10
- require_relative 'mt940/banks/triodos'
5
+ require_relative 'mt940_structured/mt940_structured'
@@ -1,3 +1,3 @@
1
1
  module MT940
2
- VERSION = '1.0'
3
- end
2
+ VERSION = '2.0'
3
+ end
@@ -0,0 +1,46 @@
1
+ class MT940Structured::FileContent
2
+ R_EOF_ING = /^-XXX$/
3
+ R_EOF_ABN_AMRO = /^-$/
4
+ R_EOF_TRIODOS = /^-$/
5
+
6
+ def initialize(raw_lines, join_lines_by = ' ')
7
+ @raw_lines = raw_lines.map{|line|line.strip}
8
+ @join_lines_by = join_lines_by
9
+ end
10
+
11
+ def get_header
12
+ MT940Structured::Header.new(@raw_lines)
13
+ end
14
+
15
+ def group_lines
16
+ body_lines = @raw_lines[start_index..(end_index-1)]
17
+ grouped_lines = []
18
+ previous_tag = nil
19
+ body_lines.each do |line|
20
+ mt940_line = line.match /^(:\d{2}[D|C|F|M]?:)/
21
+ if mt940_line && previous_tag != $1
22
+ previous_tag = $1
23
+ grouped_lines << line
24
+ else
25
+ next_line = if line.match /^(:\d{2}[D|C|F|M]?:)(.*)/
26
+ $2
27
+ else
28
+ line
29
+ end
30
+ grouped_lines[-1] = [grouped_lines.last, @join_lines_by, next_line].join
31
+ end
32
+ end
33
+ grouped_lines
34
+ end
35
+
36
+ private
37
+
38
+ def start_index
39
+ @raw_lines.index { |line| line.match /^:20:/ }
40
+ end
41
+
42
+ def end_index
43
+ @raw_lines.rindex { |line| line.match(R_EOF_ING) || line.match(R_EOF_ABN_AMRO) ||line.match(R_EOF_TRIODOS) } || 0
44
+ end
45
+
46
+ end
@@ -0,0 +1,30 @@
1
+ module MT940Structured
2
+ class Header
3
+ R_RABOBANK = /^:940:/
4
+ R_ABN_AMRO = /ABNANL/
5
+ R_TRIODOS = /^:25:TRIODOSBANK/
6
+ R_ING = /INGBNL/
7
+
8
+ def initialize(raw_lines)
9
+ @raw_lines = raw_lines
10
+ end
11
+
12
+ def parser
13
+ if @raw_lines[0].match(R_RABOBANK)
14
+ MT940Structured::Parsers::Rabobank::Parser.new
15
+ elsif @raw_lines[0].match(R_ABN_AMRO)
16
+ MT940Structured::Parsers::Abnamro::Parser.new
17
+ elsif @raw_lines[1] && @raw_lines[1].match(R_TRIODOS)
18
+ MT940Structured::Parsers::Triodos::Parser.new
19
+ elsif @raw_lines[0].match(R_ING)
20
+ MT940Structured::Parsers::Ing::Parser.new
21
+ else
22
+ raise UnsupportedBankError.new
23
+ end
24
+ end
25
+
26
+ end
27
+ class UnsupportedBankError < StandardError
28
+
29
+ end
30
+ end
@@ -0,0 +1,8 @@
1
+ module MT940Structured
2
+
3
+ end
4
+
5
+ require_relative 'parsers/parsers'
6
+ require_relative 'header'
7
+ require_relative 'file_content'
8
+ require_relative 'parser'
@@ -0,0 +1,18 @@
1
+ module MT940Structured
2
+ class Parser
3
+ def self.parse_mt940(path, join_lines_by = ' ')
4
+ file_content = FileContent.new(readfile(path), join_lines_by)
5
+ grouped_lines = file_content.group_lines
6
+ file_content.get_header.parser.transform(grouped_lines)
7
+ end
8
+
9
+ private
10
+ def self.readfile(path)
11
+ File.open(path).readlines.map do |line|
12
+ line
13
+ .encode('UTF-8', 'binary', :invalid => :replace, :undef => :replace) # remove other obscure chars. god knows what people upload.
14
+ .gsub(/\u001A/, '') # remove eof chars in the middle of the string... yes it happens :-(
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,5 @@
1
+ module MT940Structured::Parsers::Abnamro
2
+ end
3
+
4
+ require_relative 'transaction_parser'
5
+ require_relative 'parser'
@@ -0,0 +1,15 @@
1
+ module MT940Structured::Parsers::Abnamro
2
+ class Parser < MT940Structured::Parsers::Base
3
+ def initialize
4
+ super "Abnamro", TransactionParsers.new
5
+ end
6
+ end
7
+
8
+ class TransactionParsers
9
+
10
+ def for_format(_)
11
+ TransactionParser.new
12
+ end
13
+ end
14
+
15
+ end
@@ -0,0 +1,55 @@
1
+ module MT940Structured::Parsers::Abnamro
2
+ class TransactionParser
3
+ include MT940Structured::Parsers::DateParser
4
+ include MT940Structured::Parsers::IbanSupport
5
+ include MT940Structured::Parsers::StructuredDescriptionParser
6
+ include MT940Structured::Parsers::DefaultLine61Parser
7
+
8
+ def get_regex_for_line_61
9
+ /^:61:(\d{6})\d{4}(C|D)(\d+),(\d{0,2})/
10
+ end
11
+
12
+ def enrich_transaction(transaction, line_86)
13
+ transaction.contra_account = "NONREF" #default
14
+ line_86 = line_86.gsub(/:86:/, '')
15
+ case line_86
16
+ when /^(GIRO)\s+(\d+)(.+)/
17
+ transaction.contra_account = $2.rjust(9, '000000000')
18
+ transaction.description = $3.strip
19
+ when /^(\d{2}.\d{2}.\d{2}.\d{3})(.+)/
20
+ transaction.description = $2.strip
21
+ transaction.contra_account = $1.gsub('.', '')
22
+ when /\/TRTP\/SEPA OVERBOEKING/
23
+ description_parts = line_86[4..-1].split('/')
24
+ transaction.contra_account_iban = parse_description_after_tag description_parts, "IBAN"
25
+ transaction.contra_account = iban_to_account transaction.contra_account_iban
26
+ transaction.contra_account_owner = parse_description_after_tag description_parts, "NAME"
27
+ transaction.description = parse_description_after_tag description_parts, "REMI"
28
+ when /SEPA IDEAL/
29
+ if line_86.match /OMSCHRIJVING\:(.+)?/
30
+ transaction.description = $1.strip
31
+ end
32
+ if line_86.match /IBAN\:(.+)?BIC\:/
33
+ transaction.contra_account_iban = $1.strip
34
+ transaction.contra_account = iban_to_account transaction.contra_account_iban
35
+ end
36
+ if line_86.match /NAAM\:(.+)?OMSCHRIJVING\:/
37
+ transaction.contra_account_owner = $1.strip
38
+ end
39
+ when /SEPA ACCEPTGIROBETALING/
40
+ if line_86.match /(BETALINGSKENM\.\:.+)/
41
+ transaction.description = $1.strip
42
+ end
43
+ if line_86.match /IBAN\:(.+)?BIC\:/
44
+ transaction.contra_account_iban = $1.strip
45
+ transaction.contra_account = iban_to_account transaction.contra_account_iban
46
+ end
47
+ if line_86.match /NAAM\:(.+)?BETALINGSKENM\.\:/
48
+ transaction.contra_account_owner = $1.strip
49
+ end
50
+ else
51
+ transaction.description = line_86
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,12 @@
1
+ module MT940Structured::Parsers
2
+ module BalanceParser
3
+ def parse_balance(line)
4
+ currency = line[12..14]
5
+ balance_date = parse_date(line[6..11])
6
+ type = line[5] == 'D' ? -1 : 1
7
+ amount = line[15..-1].gsub(",", ".").to_f * type
8
+ MT940::Balance.new(amount, balance_date, currency)
9
+ end
10
+
11
+ end
12
+ end
@@ -0,0 +1,59 @@
1
+ module MT940Structured::Parsers
2
+ class BankStatementParser
3
+ include DateParser, BalanceParser, IbanSupport
4
+ attr_reader :bank_statement
5
+
6
+ def initialize(bank, transaction_parsers, lines)
7
+ @bank = bank
8
+ @transaction_parsers = transaction_parsers
9
+ @bank_statement = MT940::BankStatement.new([])
10
+ lines.each do |line|
11
+ if line.match /^:(\d{2})(F|C|M)?:/
12
+ parse_method = "parse_line_#{$1}".to_sym
13
+ send(parse_method, line) if respond_to? parse_method
14
+ else
15
+ raise "nyi '#{$1}' - line #{line}"
16
+ end
17
+ end
18
+ end
19
+
20
+ def parse_line_25(line)
21
+ line.gsub!('.', '')
22
+ case line
23
+ when /^:\d{2}:NL/
24
+ @bank_statement.bank_account_iban = line[4, 18]
25
+ @bank_statement.bank_account = iban_to_account(@bank_statement.bank_account_iban)
26
+ @is_structured_format = true
27
+ when /^:\d{2}:\D*(\d*)/
28
+ @bank_statement.bank_account = $1.gsub(/\D/, '').gsub(/^0+/, '')
29
+ @is_structured_format = false
30
+ else
31
+ raise "Unknown format for tag 25: #{line}"
32
+ end
33
+ end
34
+
35
+ def parse_line_60(line)
36
+ @bank_statement.previous_balance = parse_balance(line)
37
+ end
38
+
39
+ def parse_line_61(line_61)
40
+ @is_structured_format = @transaction_parsers.structured?(line_61) if @transaction_parsers.respond_to?(:structured?)
41
+ @transaction_parser = @transaction_parsers.for_format @is_structured_format
42
+ transaction = @transaction_parser.parse_transaction(line_61)
43
+ transaction.bank_account = @bank_statement.bank_account
44
+ transaction.bank_account_iban = @bank_statement.bank_account_iban
45
+ transaction.currency = @bank_statement.previous_balance.currency
46
+ transaction.bank = @bank
47
+ @bank_statement.transactions << transaction
48
+ end
49
+
50
+ def parse_line_86(line)
51
+ @transaction_parser.enrich_transaction(@bank_statement.transactions.last, line)
52
+ end
53
+
54
+ def parse_line_62(line)
55
+ @bank_statement.new_balance = parse_balance(line)
56
+ end
57
+ end
58
+
59
+ end
@@ -0,0 +1,34 @@
1
+ module MT940Structured::Parsers
2
+ class Base
3
+ def initialize(bank, transaction_parsers)
4
+ @bank = bank
5
+ @transaction_parsers = transaction_parsers
6
+ end
7
+
8
+ def transform(lines)
9
+ bank_statements = Hash.new { |h, k| h[k] = [] }
10
+ result = group_lines_by_tag(lines)
11
+ result.each do |bank_statement_lines|
12
+ bank_statement = BankStatementParser.new(@bank, @transaction_parsers, bank_statement_lines).bank_statement
13
+ bank_statements[bank_statement.bank_account] << bank_statement
14
+ end
15
+ bank_statements
16
+ end
17
+
18
+ private
19
+ def group_lines_by_tag(lines)
20
+ result = []
21
+ while !lines.empty? do
22
+ start_index = lines.index { |line| line.match(/^:20:/)}
23
+ end_index = lines.index { |line| line.match(/^:62F:/)}
24
+ if start_index && end_index > start_index
25
+ result << lines[start_index..end_index]
26
+ lines = lines.drop(end_index + 1)
27
+ else
28
+ lines = []
29
+ end
30
+ end
31
+ result
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,7 @@
1
+ module MT940Structured::Parsers
2
+ module DateParser
3
+ def parse_date(string)
4
+ Date.new(2000 + string[0..1].to_i, string[2..3].to_i, string[4..5].to_i) if string
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,25 @@
1
+ module MT940Structured::Parsers
2
+ ##
3
+ # Basic line 61 parser. Retrieves the date and amount from the line :61:.
4
+ # This module expects that a method get_regex_for_line_61 exists that returns
5
+ # a regex that will, if matched, produces the following groups:
6
+ # $1 - the transaction date
7
+ # $2 - D for Debit, C for Credit transactions
8
+ # $3 - The amount of the transaction before the cent mark.
9
+ # $4 - The cents of the transaction
10
+ #
11
+ module DefaultLine61Parser
12
+ def get_regex_for_line_61
13
+ raise 'Override this when using this module'
14
+ end
15
+
16
+ def parse_transaction(line_61)
17
+ if line_61.match(get_regex_for_line_61)
18
+ type = $2 == 'D' ? -1 : 1
19
+ transaction = MT940::Transaction.new(amount: type * ($3 + '.' + $4).to_f)
20
+ transaction.date = parse_date($1)
21
+ transaction
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,15 @@
1
+ module MT940Structured::Parsers
2
+ module IbanSupport
3
+ IBAN_R = /[a-zA-Z]{2}[0-9]{2}[a-zA-Z0-9]{0,30}/
4
+
5
+ def iban?(string)
6
+ !string.nil? and string.match(IBAN_R)
7
+ end
8
+
9
+ def iban_to_account(iban)
10
+ !iban.nil? ? iban.split(//).last(10).join.gsub(/^0+/, '') : nil
11
+ end
12
+
13
+ end
14
+
15
+ end
@@ -0,0 +1,7 @@
1
+ module MT940Structured::Parsers::Ing
2
+ end
3
+
4
+ require_relative 'types'
5
+ require_relative 'transaction_parser'
6
+ require_relative 'structured_transaction_parser'
7
+ require_relative 'parser'
@@ -0,0 +1,18 @@
1
+ module MT940Structured::Parsers::Ing
2
+ class Parser < MT940Structured::Parsers::Base
3
+ def initialize
4
+ super "Ing", TransactionParsers.new
5
+ end
6
+ end
7
+
8
+ class TransactionParsers
9
+ def structured?(line_61)
10
+ line_61.match /EREF|PREF|MARF|\d{16}/
11
+ end
12
+
13
+ def for_format(is_structured)
14
+ is_structured ? StructuredTransactionParser.new : TransactionParser.new
15
+ end
16
+ end
17
+
18
+ end