cmxl 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +136 -0
- data/Rakefile +7 -0
- data/cmxl.gemspec +24 -0
- data/lib/cmxl.rb +20 -0
- data/lib/cmxl/field.rb +78 -0
- data/lib/cmxl/fields/account_balance.rb +45 -0
- data/lib/cmxl/fields/account_identification.rb +12 -0
- data/lib/cmxl/fields/available_balance.rb +8 -0
- data/lib/cmxl/fields/closing_balance.rb +8 -0
- data/lib/cmxl/fields/reference.rb +21 -0
- data/lib/cmxl/fields/statement_details.rb +54 -0
- data/lib/cmxl/fields/statement_line.rb +47 -0
- data/lib/cmxl/fields/statement_number.rb +8 -0
- data/lib/cmxl/statement.rb +84 -0
- data/lib/cmxl/transaction.rb +91 -0
- data/lib/cmxl/version.rb +3 -0
- data/spec/fields/account_balance_spec.rb +41 -0
- data/spec/fields/account_identification_spec.rb +24 -0
- data/spec/fields/available_balance_spec.rb +16 -0
- data/spec/fields/closing_balance_spec.rb +15 -0
- data/spec/fields/reference_spec.rb +12 -0
- data/spec/fields/statement_details_spec.rb +40 -0
- data/spec/fields/statement_number_spec.rb +10 -0
- data/spec/fields/statment_line_spec.rb +19 -0
- data/spec/fields/unknown_spec.rb +9 -0
- data/spec/fixtures/lines/account_balance_credit.txt +1 -0
- data/spec/fixtures/lines/account_balance_debit.txt +1 -0
- data/spec/fixtures/lines/account_identification_iban.txt +1 -0
- data/spec/fixtures/lines/account_identification_legacy.txt +1 -0
- data/spec/fixtures/lines/available_balance.txt +1 -0
- data/spec/fixtures/lines/closing_balance.txt +1 -0
- data/spec/fixtures/lines/reference.txt +1 -0
- data/spec/fixtures/lines/statement_details.txt +1 -0
- data/spec/fixtures/lines/statement_line.txt +1 -0
- data/spec/fixtures/lines/statement_number.txt +1 -0
- data/spec/fixtures/mt940.txt +75 -0
- data/spec/mt940_parsing_spec.rb +44 -0
- data/spec/spec_helper.rb +30 -0
- data/spec/support/fixtures.rb +7 -0
- data/spec/transaction_spec.rb +28 -0
- 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,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
|
data/lib/cmxl/version.rb
ADDED
@@ -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
|