zilverline-mt940 1.0 → 2.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.
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