cmxl 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,47 @@
1
+ module Cmxl
2
+ module Fields
3
+ class StatementLine < Field
4
+ self.tag = 61
5
+ self.parser = /\A(?<date>\d{6})(?<entry_date>\d{4})?(?<funds_code>[a-zA-Z])(?<currency_letter>[a-zA-Z])\D?(?<amount>\d{1,12},\d{0,2})(?<swift_code>(?:N|F).{3})(?<reference>NONREF|.{0,16})(?:$|\/\/)(?<bank_reference>.*)/i
6
+
7
+ def credit?
8
+ self.data['funds_code'].to_s.upcase == 'C'
9
+ end
10
+
11
+ def debit?
12
+ !credit?
13
+ end
14
+
15
+ def sign
16
+ self.credit? ? 1 : -1
17
+ end
18
+
19
+ def amount
20
+ self.data['amount'].gsub(',','.').to_f * sign
21
+ end
22
+
23
+ def amount_in_cents
24
+ self.data['amount'].gsub(',', '').gsub('.','').to_i * sign
25
+ end
26
+
27
+ def date
28
+ to_date(self.data['date'])
29
+ end
30
+ def entry_date
31
+ to_date(self.data['entry_date'], self.date.year)
32
+ end
33
+
34
+ def to_h
35
+ super.merge({
36
+ 'date' => date,
37
+ 'entry_date' => entry_date,
38
+ 'amount' => amount,
39
+ 'amount_in_cents' => amount_in_cents,
40
+ 'sign' => sign,
41
+ 'debit' => debit?,
42
+ 'credit' => credit?
43
+ })
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,8 @@
1
+ module Cmxl
2
+ module Fields
3
+ class StatementNumber < Field
4
+ self.tag = 28
5
+ self.parser = /(?<statement_number>\d{5})\/(?<sequence_number>\d{3,5})/
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,84 @@
1
+ require 'digest/sha2'
2
+ module Cmxl
3
+ class Statement
4
+ attr_accessor :source, :collection, :transactions, :fields, :lines
5
+
6
+ def initialize(source)
7
+ self.source = source
8
+ self.fields = []
9
+ self.lines = []
10
+ self.transactions = []
11
+ self.parse!
12
+ end
13
+
14
+ def parse!
15
+ # first we clean uo the source and make concat wraped lines
16
+ self.source.split("\n").each do |line|
17
+ if line.start_with?(':') || self.lines.last.nil?
18
+ self.lines << line.strip
19
+ else
20
+ self.lines.last << line.strip
21
+ end
22
+ end
23
+ # now we check each line. if it is part of a transaction we initate or update a transaction else we parse the field and add it to the fields collection
24
+ self.lines.each do |line|
25
+ if line.match(/\A:61:/)
26
+ self.transactions << Cmxl::Transaction.new(line)
27
+ elsif line.match(/\A:86:/) && !self.transactions.last.nil?
28
+ self.transactions.last.details = line
29
+ else
30
+ self.fields << Field.parse(line)
31
+ end
32
+ end
33
+ end
34
+
35
+ def sha
36
+ Digest::SHA2.new.update(self.source).to_s
37
+ end
38
+
39
+ def reference
40
+ self.field(20).reference
41
+ end
42
+
43
+ def generation_date
44
+ self.field(20).date
45
+ end
46
+
47
+ def account_identification
48
+ self.field(25)
49
+ end
50
+
51
+ def opening_balance
52
+ self.field(60, 'F')
53
+ end
54
+
55
+ def closing_balance
56
+ self.field(62, 'F')
57
+ end
58
+
59
+ def available_balance
60
+ self.field(64)
61
+ end
62
+
63
+ def to_h
64
+ {
65
+ 'reference' => reference,
66
+ 'sha' => sha,
67
+ 'generation_date' => generation_date,
68
+ 'account_identification' => account_identification,
69
+ 'opening_balance' => opening_balance,
70
+ 'closing_balance' => closing_balance,
71
+ 'available_balance' => available_balance,
72
+ 'transactions' => transactions
73
+ }
74
+ end
75
+ alias :to_hash :to_h
76
+ def to_json(*args)
77
+ to_h.to_json(*args)
78
+ end
79
+
80
+ def field(tag, modifier=nil)
81
+ self.fields.detect {|field| field.tag == tag.to_s && (modifier.nil? || field.modifier == modifier) }
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,91 @@
1
+ module Cmxl
2
+ class Transaction
3
+ attr_accessor :statement_line, :details
4
+
5
+ def initialize(statement_line, details = nil)
6
+ self.statement_line = statement_line
7
+ self.details = details
8
+ end
9
+
10
+ def statement_line=(line)
11
+ @statement_line = Cmxl::Fields::StatementLine.parse(line) unless line.nil?
12
+ end
13
+
14
+ def details=(line)
15
+ @details = Cmxl::Fields::StatementDetails.parse(line) unless line.nil?
16
+ end
17
+
18
+ def debit?
19
+ self.statement_line.debit?
20
+ end
21
+ def credit?
22
+ self.statement_line.credit?
23
+ end
24
+ def amount
25
+ self.statement_line.amount
26
+ end
27
+ def sign
28
+ self.statement_line.sign
29
+ end
30
+ def funds_code
31
+ self.statement_line.funds_code
32
+ end
33
+ def amount_in_cents
34
+ self.statement_line.amount_in_cents
35
+ end
36
+ def date
37
+ self.statement_line.date
38
+ end
39
+ def entry_date
40
+ self.statement_line.entry_date
41
+ end
42
+ def funds_code
43
+ self.statement_line.funds_code
44
+ end
45
+ def currency_letter
46
+ self.statement_line.currency_letter
47
+ end
48
+ def swift_code
49
+ self.statement_line.swift_code
50
+ end
51
+ def reference
52
+ self.statement_line.reference
53
+ end
54
+ def bank_reference
55
+ self.statement_line.bank_reference
56
+ end
57
+ def description
58
+ self.details.description if self.details
59
+ end
60
+ def information
61
+ self.details.information if self.details
62
+ end
63
+ def bic
64
+ self.details.bic if self.details
65
+ end
66
+ def name
67
+ self.details.name if self.details
68
+ end
69
+ def iban
70
+ self.details.iban if self.details
71
+ end
72
+ def sepa
73
+ self.details.sepa if self.details
74
+ end
75
+ def sub_fields
76
+ self.details.sub_fields if self.details
77
+ end
78
+
79
+ def to_h
80
+ {}.tap do |h|
81
+ h.merge!(self.statement_line.to_h)
82
+ h.merge!(self.details.to_h) if self.details
83
+ end
84
+ end
85
+ alias :to_hash :to_h
86
+ def to_json(*args)
87
+ to_h.to_json(*args)
88
+ end
89
+ end
90
+
91
+ end
@@ -0,0 +1,3 @@
1
+ module Cmxl
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+ require 'date'
3
+ describe Cmxl::Fields::AccountBalance do
4
+
5
+ context 'Credit' do
6
+ subject { Cmxl::Fields::AccountBalance.parse( fixture_line(:account_balance_credit) ) }
7
+
8
+ it { expect(subject.date).to eql(Date.new(2014,8,29)) }
9
+ it { expect(subject).to be_credit }
10
+ it { expect(subject).to_not be_debit }
11
+ it { expect(subject.currency).to eql('EUR') }
12
+
13
+ it { expect(subject.amount).to eql(147.64) }
14
+ it { expect(subject.amount_in_cents).to eql(14764) }
15
+ it { expect(subject.sign).to eql(1) }
16
+ it { expect(subject.to_h).to eql({
17
+ 'date' =>Date.new(2014,8,29),
18
+ 'funds_code' =>"C",
19
+ 'credit' =>true,
20
+ 'debit' =>false,
21
+ 'currency' =>"EUR",
22
+ 'amount' =>147.64,
23
+ 'amount_in_cents' =>14764,
24
+ 'sign' =>1
25
+ }) }
26
+ end
27
+
28
+ context 'Debit' do
29
+ subject { Cmxl::Fields::AccountBalance.parse( fixture_line(:account_balance_debit) ) }
30
+
31
+ it { expect(subject.date).to eql(Date.new(2014,8,29)) }
32
+ it { expect(subject).to_not be_credit }
33
+ it { expect(subject).to be_debit }
34
+ it { expect(subject.currency).to eql('EUR') }
35
+
36
+ it { expect(subject.amount).to eql(-147.64) }
37
+ it { expect(subject.amount_in_cents).to eql(-14764) }
38
+ it { expect(subject.sign).to eql(-1) }
39
+ end
40
+
41
+ end
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+
3
+ describe Cmxl::Fields::AccountIdentification do
4
+
5
+ context 'legacy data' do
6
+ subject { Cmxl::Fields::AccountBalance.parse( fixture_line(:account_identification_legacy) ) }
7
+
8
+ it { expect(subject.bank_code).to eql('70011110') }
9
+ it { expect(subject.account_number).to eql('4005287001') }
10
+ it { expect(subject.currency).to eql('EUR') }
11
+ it { expect(subject.country).to be_nil }
12
+
13
+ end
14
+
15
+ context 'iban' do
16
+ subject { Cmxl::Fields::AccountBalance.parse( fixture_line(:account_identification_iban) ) }
17
+ it { expect(subject.country).to eql('PL') }
18
+ it { expect(subject.ban).to eql('25106000760000888888888888') }
19
+ it { expect(subject.iban).to eql('PL25106000760000888888888888') }
20
+ it { expect(subject.bank_code).to be_nil }
21
+
22
+ end
23
+
24
+ end
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+
3
+ describe Cmxl::Fields::AvailableBalance do
4
+
5
+ subject { Cmxl::Fields::AvailableBalance.parse(fixture_line(:available_balance)) }
6
+
7
+ it { expect(subject.date).to eql(Date.new(2014,9,1)) }
8
+ it { expect(subject).to_not be_credit }
9
+ it { expect(subject).to be_debit }
10
+ it { expect(subject.currency).to eql('EUR') }
11
+
12
+ it { expect(subject.amount).to eql(-3.66) }
13
+ it { expect(subject.amount_in_cents).to eql(-366) }
14
+ it { expect(subject.sign).to eql(-1) }
15
+
16
+ end
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+
3
+ describe Cmxl::Fields::ClosingBalance do
4
+
5
+ subject { Cmxl::Fields::ClosingBalance.parse(fixture_line(:closing_balance)) }
6
+
7
+ it { expect(subject.date).to eql(Date.new(2014,9,1)) }
8
+ it { expect(subject).to be_credit }
9
+ it { expect(subject).to_not be_debit }
10
+
11
+ it { expect(subject.amount).to eql(137.0) }
12
+ it { expect(subject.amount_in_cents).to eql(13700) }
13
+ it { expect(subject.sign).to eql(1) }
14
+
15
+ end
@@ -0,0 +1,12 @@
1
+ require 'spec_helper'
2
+
3
+ describe Cmxl::Fields::Reference do
4
+
5
+ subject { Cmxl::Fields::Reference.parse(fixture_line(:reference)) }
6
+
7
+ it { expect(subject.reference).to eql('D140902049') }
8
+ it { expect(subject.date).to eql(Date.new(2014,9,2)) }
9
+ it { expect(subject.statement_identifier).to eql('D') }
10
+ it { expect(subject.additional_number).to eql('049') }
11
+ it { expect(subject.to_h).to eql({"statement_identifier"=>"D", "date"=> Date.new(2014,9,2), "additional_number"=>"049", "reference"=>"D140902049"}) }
12
+ end
@@ -0,0 +1,40 @@
1
+ require 'spec_helper'
2
+
3
+ describe Cmxl::Fields::StatementDetails do
4
+
5
+ context 'sepa' do
6
+ subject { Cmxl::Fields::StatementDetails.parse(fixture_line(:statement_details)) }
7
+
8
+ it { expect(subject.transaction_code).to eql('171') }
9
+ it { expect(subject.seperator).to eql('?') }
10
+ it { expect(subject.description).to eql('SEPA LASTSCHRIFT KUNDE') }
11
+ it { expect(subject.information).to eql('EREF+TRX-0A4A47C3-F846-4729-8A1B-5DF620FMREF+CAC97D2144174318AC18D9BF815BD4FBCRED+DE98ZZZ09999999999SVWZ+FOO TRX-0A4A47C3-F846-4729-8A1B-5DF620F') }
12
+ it { expect(subject.bic).to eql('HYVEDEMMXXX' ) }
13
+ it { expect(subject.name).to eql('Peter Pan') }
14
+ it { expect(subject.iban).to eql('HUkkbbbsssskcccccccccccccccx') }
15
+ it { expect(subject.sub_fields).to eql({
16
+ "00" => "SEPA LASTSCHRIFT KUNDE",
17
+ "10" => "281",
18
+ "20" => "EREF+TRX-0A4A47C3-F846-4729",
19
+ "21" => "-8A1B-5DF620F",
20
+ "22" => "MREF+CAC97D2144174318AC18D9",
21
+ "23" => "BF815BD4FB",
22
+ "24" => "CRED+DE98ZZZ09999999999",
23
+ "25" => "SVWZ+FOO TRX-0A4A47C3-F84",
24
+ "26" => "6-4729-8A1B-5DF620F",
25
+ "30" => "HYVEDEMMXXX",
26
+ "31" => "HUkkbbbsssskcccccccccccccccx",
27
+ "32" => "Peter Pan",
28
+ "34" => "171"
29
+ }) }
30
+ it { expect(subject.sepa).to eql({
31
+ "CRED" => "DE98ZZZ09999999999",
32
+ "EREF" => "TRX-0A4A47C3-F846-4729-8A1B-5DF620F",
33
+ "MREF" => "CAC97D2144174318AC18D9BF815BD4FB",
34
+ "SVWZ" => "FOO TRX-0A4A47C3-F846-4729-8A1B-5DF620F"
35
+ }) }
36
+
37
+ it { expect(subject.to_h).to eql({'bic'=>"HYVEDEMMXXX", 'iban'=>"HUkkbbbsssskcccccccccccccccx", 'name' => "Peter Pan", 'sepa' => {"EREF"=>"TRX-0A4A47C3-F846-4729-8A1B-5DF620F", "MREF"=>"CAC97D2144174318AC18D9BF815BD4FB", "CRED"=>"DE98ZZZ09999999999", "SVWZ"=>"FOO TRX-0A4A47C3-F846-4729-8A1B-5DF620F"}, 'information' => "EREF+TRX-0A4A47C3-F846-4729-8A1B-5DF620FMREF+CAC97D2144174318AC18D9BF815BD4FBCRED+DE98ZZZ09999999999SVWZ+FOO TRX-0A4A47C3-F846-4729-8A1B-5DF620F", 'description' => "SEPA LASTSCHRIFT KUNDE", 'sub_fields' => {"00"=>"SEPA LASTSCHRIFT KUNDE", "10"=>"281", "20"=>"EREF+TRX-0A4A47C3-F846-4729", "21"=>"-8A1B-5DF620F", "22"=>"MREF+CAC97D2144174318AC18D9", "23"=>"BF815BD4FB", "24"=>"CRED+DE98ZZZ09999999999", "25"=>"SVWZ+FOO TRX-0A4A47C3-F84", "26"=>"6-4729-8A1B-5DF620F", "30"=>"HYVEDEMMXXX", "31"=>"HUkkbbbsssskcccccccccccccccx", "32"=>"Peter Pan", "34"=>"171"}, 'transaction_code' => "171", 'details' => "?00SEPA LASTSCHRIFT KUNDE?10281?20EREF+TRX-0A4A47C3-F846-4729?21-8A1B-5DF620F?22MREF+CAC97D2144174318AC18D9?23BF815BD4FB?24CRED+DE98ZZZ09999999999?25SVWZ+FOO TRX-0A4A47C3-F84?266-4729-8A1B-5DF620F?30HYVEDEMMXXX?31HUkkbbbsssskcccccccccccccccx?32Peter Pan?34171"}) }
38
+ end
39
+
40
+ end
@@ -0,0 +1,10 @@
1
+ require 'spec_helper'
2
+
3
+ describe Cmxl::Fields::StatementNumber do
4
+
5
+ subject { Cmxl::Fields::StatementNumber.parse(fixture_line(:statement_number)) }
6
+
7
+ it { expect(subject.statement_number).to eql('00035') }
8
+ it { expect(subject.sequence_number).to eql('001') }
9
+
10
+ end
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+
3
+ describe Cmxl::Fields::StatementLine do
4
+
5
+ subject { Cmxl::Fields::StatementLine.parse(fixture_line(:statement_line)) }
6
+
7
+ it { expect(subject.date).to eql(Date.new(2014,9,1)) }
8
+ it { expect(subject.entry_date).to eql(Date.new(2014,9,2)) }
9
+ it { expect(subject.funds_code).to eql('D') }
10
+ it { expect(subject.currency_letter).to eql('R') }
11
+ it { expect(subject.amount).to eql(-1.62) }
12
+ it { expect(subject.amount_in_cents).to eql(-162) }
13
+ it { expect(subject.swift_code).to eql('NTRF') }
14
+ it { expect(subject.reference).to eql('0000549855700010') }
15
+ it { expect(subject.bank_reference).to eql('025498557/000001') }
16
+ it { expect(subject).to_not be_credit }
17
+ it { expect(subject).to be_debit }
18
+ it { expect(subject.sign).to eql(-1) }
19
+ end
@@ -0,0 +1,9 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Unknwn fields' do
4
+
5
+ subject { Cmxl::Field.parse(':42F:C140908EUR000000000136,02') }
6
+ it { expect(subject.tag).to eql('42') }
7
+ it { expect(subject.source).to eql('C140908EUR000000000136,02') }
8
+ it { expect(subject.to_h).to eql({tag: '42', modifier: 'F', source: 'C140908EUR000000000136,02'}) }
9
+ end
@@ -0,0 +1 @@
1
+ :60F:C140829EUR000000000147,64