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
@@ -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