cmxl 1.2.0 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +5 -2
- data/CHANGELOG.mdown +28 -18
- data/README.md +45 -26
- data/cmxl.gemspec +15 -16
- data/lib/cmxl.rb +9 -9
- data/lib/cmxl/field.rb +25 -18
- data/lib/cmxl/fields/account_balance.rb +7 -7
- data/lib/cmxl/fields/account_identification.rb +1 -1
- data/lib/cmxl/fields/floor_limit_indicator.rb +20 -0
- data/lib/cmxl/fields/reference.rb +3 -4
- data/lib/cmxl/fields/statement_details.rb +13 -13
- data/lib/cmxl/fields/transaction.rb +10 -1
- data/lib/cmxl/fields/transaction_supplementary.rb +11 -8
- data/lib/cmxl/fields/vmk_summary.rb +33 -0
- data/lib/cmxl/statement.rb +52 -22
- data/lib/cmxl/version.rb +1 -1
- data/spec/field_spec.rb +8 -9
- data/spec/fields/account_balance_spec.rb +18 -18
- data/spec/fields/account_identification_spec.rb +2 -6
- data/spec/fields/available_balance_spec.rb +1 -3
- data/spec/fields/closing_balance_spec.rb +2 -4
- data/spec/fields/entry_date_spec.rb +1 -1
- data/spec/fields/floor_limit_indicator_spec.rb +30 -0
- data/spec/fields/reference_spec.rb +2 -3
- data/spec/fields/statement_details_spec.rb +61 -56
- data/spec/fields/statement_number_spec.rb +0 -2
- data/spec/fields/transaction_spec.rb +4 -4
- data/spec/fields/transaction_supplementary_spec.rb +4 -4
- data/spec/fields/unknown_spec.rb +1 -2
- data/spec/fields/vmk_summary_spec.rb +23 -0
- data/spec/fixtures/lines/floor_limit_indicator_both.txt +1 -0
- data/spec/fixtures/lines/floor_limit_indicator_credit.txt +1 -0
- data/spec/fixtures/lines/floor_limit_indicator_debit.txt +1 -0
- data/spec/fixtures/lines/vmk_summary_credit.txt +1 -0
- data/spec/fixtures/lines/vmk_summary_debit.txt +1 -0
- data/spec/fixtures/mt942.txt +9 -0
- data/spec/fixtures/statement-details-mt942.txt +4 -0
- data/spec/mt940_parsing_spec.rb +13 -14
- data/spec/spec_helper.rb +3 -6
- data/spec/statement_spec.rb +119 -99
- data/spec/support/fixtures.rb +1 -1
- metadata +39 -20
@@ -5,11 +5,11 @@ module Cmxl
|
|
5
5
|
self.parser = /(?<funds_code>\A[a-zA-Z]{1})(?<date>\d{6})(?<currency>[a-zA-Z]{3})(?<amount>[\d|,|\.]{4,15})/i
|
6
6
|
|
7
7
|
def date
|
8
|
-
to_date(
|
8
|
+
to_date(data['date'])
|
9
9
|
end
|
10
10
|
|
11
11
|
def credit?
|
12
|
-
|
12
|
+
data['funds_code'].to_s.casecmp('C').zero?
|
13
13
|
end
|
14
14
|
|
15
15
|
def debit?
|
@@ -17,19 +17,19 @@ module Cmxl
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def amount
|
20
|
-
to_amount(
|
20
|
+
to_amount(data['amount'])
|
21
21
|
end
|
22
22
|
|
23
23
|
def sign
|
24
|
-
|
24
|
+
credit? ? 1 : -1
|
25
25
|
end
|
26
26
|
|
27
27
|
def amount_in_cents
|
28
|
-
to_amount_in_cents(
|
28
|
+
to_amount_in_cents(data['amount'])
|
29
29
|
end
|
30
30
|
|
31
31
|
def to_h
|
32
|
-
super.merge(
|
32
|
+
super.merge(
|
33
33
|
'date' => date,
|
34
34
|
'funds_code' => funds_code,
|
35
35
|
'credit' => credit?,
|
@@ -38,7 +38,7 @@ module Cmxl
|
|
38
38
|
'amount' => amount,
|
39
39
|
'amount_in_cents' => amount_in_cents,
|
40
40
|
'sign' => sign
|
41
|
-
|
41
|
+
)
|
42
42
|
end
|
43
43
|
end
|
44
44
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Cmxl
|
2
|
+
module Fields
|
3
|
+
class FloorLimitIndicator < Field
|
4
|
+
self.tag = 34
|
5
|
+
self.parser = /(?<currency>[a-zA-Z]{3})(?<type_indicator>[DC]?)(?<amount>[\d|,|\.]{4,15})/i
|
6
|
+
|
7
|
+
def credit?
|
8
|
+
data['type_indicator'].empty? || data['type_indicator'] == 'C'
|
9
|
+
end
|
10
|
+
|
11
|
+
def debit?
|
12
|
+
data['type_indicator'].empty? || data['type_indicator'] == 'D'
|
13
|
+
end
|
14
|
+
|
15
|
+
def amount
|
16
|
+
to_amount(data['amount'])
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -5,17 +5,16 @@ module Cmxl
|
|
5
5
|
self.parser = /(?<statement_identifier>[a-zA-Z]{0,2})(?<date>\d{6})(?<additional_number>.*)/i
|
6
6
|
|
7
7
|
def reference
|
8
|
-
|
8
|
+
source
|
9
9
|
end
|
10
10
|
|
11
11
|
def date
|
12
|
-
to_date(
|
12
|
+
to_date(data['date'])
|
13
13
|
end
|
14
14
|
|
15
15
|
def to_h
|
16
|
-
super.merge(
|
16
|
+
super.merge('date' => date, 'reference' => source)
|
17
17
|
end
|
18
|
-
|
19
18
|
end
|
20
19
|
end
|
21
20
|
end
|
@@ -7,31 +7,31 @@ module Cmxl
|
|
7
7
|
class << self
|
8
8
|
def parse(line)
|
9
9
|
# remove line breaks as they are allowed via documentation but not needed for data-parsing
|
10
|
-
super line.
|
10
|
+
super line.delete("\n")
|
11
11
|
end
|
12
12
|
end
|
13
13
|
|
14
14
|
def sub_fields
|
15
|
-
@sub_fields ||= if
|
16
|
-
|
17
|
-
|
18
|
-
|
15
|
+
@sub_fields ||= if data['details'] =~ /#{Regexp.escape(data['seperator'])}(\d{2})/
|
16
|
+
Hash[data['details'].scan(/#{Regexp.escape(data['seperator'])}(\d{2})([^#{Regexp.escape(data['seperator'])}]*)/)]
|
17
|
+
else
|
18
|
+
{}
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
22
22
|
def description
|
23
|
-
|
23
|
+
sub_fields['00'] || data['details']
|
24
24
|
end
|
25
25
|
|
26
26
|
def information
|
27
|
-
info = (20..29).to_a.collect {|i|
|
28
|
-
info.empty? ?
|
27
|
+
info = (20..29).to_a.collect { |i| sub_fields[i.to_s] }.join('')
|
28
|
+
info.empty? ? description : info
|
29
29
|
end
|
30
30
|
|
31
31
|
def sepa
|
32
|
-
if
|
32
|
+
if information =~ /([A-Z]{4})\+/
|
33
33
|
Hash[
|
34
|
-
*
|
34
|
+
*information.split(/([A-Z]{4})\+/)[1..-1].tap { |info| info << '' if info.size.odd? }
|
35
35
|
]
|
36
36
|
else
|
37
37
|
{}
|
@@ -39,15 +39,15 @@ module Cmxl
|
|
39
39
|
end
|
40
40
|
|
41
41
|
def bic
|
42
|
-
|
42
|
+
sub_fields['30']
|
43
43
|
end
|
44
44
|
|
45
45
|
def name
|
46
|
-
[
|
46
|
+
[sub_fields['32'], sub_fields['33']].compact.join(' ')
|
47
47
|
end
|
48
48
|
|
49
49
|
def iban
|
50
|
-
|
50
|
+
sub_fields['38'] || sub_fields['31']
|
51
51
|
end
|
52
52
|
|
53
53
|
def to_h
|
@@ -77,12 +77,15 @@ module Cmxl
|
|
77
77
|
def initial_amount_in_cents
|
78
78
|
supplementary.initial_amount_in_cents
|
79
79
|
end
|
80
|
+
|
80
81
|
def initial_currency
|
81
82
|
supplementary.initial_currency
|
82
83
|
end
|
84
|
+
|
83
85
|
def charges_in_cents
|
84
86
|
supplementary.charges_in_cents
|
85
87
|
end
|
88
|
+
|
86
89
|
def charges_currency
|
87
90
|
supplementary.charges_currency
|
88
91
|
end
|
@@ -92,21 +95,27 @@ module Cmxl
|
|
92
95
|
def description
|
93
96
|
details.description if details
|
94
97
|
end
|
98
|
+
|
95
99
|
def information
|
96
100
|
details.information if details
|
97
101
|
end
|
102
|
+
|
98
103
|
def bic
|
99
104
|
details.bic if details
|
100
105
|
end
|
106
|
+
|
101
107
|
def name
|
102
108
|
details.name if details
|
103
109
|
end
|
110
|
+
|
104
111
|
def iban
|
105
112
|
details.iban if details
|
106
113
|
end
|
114
|
+
|
107
115
|
def sepa
|
108
116
|
details.sepa if details
|
109
117
|
end
|
118
|
+
|
110
119
|
def sub_fields
|
111
120
|
details.sub_fields if details
|
112
121
|
end
|
@@ -126,7 +135,7 @@ module Cmxl
|
|
126
135
|
'swift_code' => swift_code,
|
127
136
|
'reference' => reference,
|
128
137
|
'bank_reference' => bank_reference,
|
129
|
-
'currency_letter' => currency_letter
|
138
|
+
'currency_letter' => currency_letter
|
130
139
|
}.tap do |h|
|
131
140
|
h.merge!(details.to_h) if details
|
132
141
|
h.merge!(supplementary.to_h) if supplementary.source
|
@@ -1,20 +1,23 @@
|
|
1
1
|
module Cmxl
|
2
2
|
module Fields
|
3
3
|
class TransactionSupplementary < Field
|
4
|
-
|
5
4
|
attr_accessor :source, :initial, :charges
|
6
5
|
|
7
6
|
class << self
|
8
7
|
def parse(line)
|
9
|
-
initial =
|
10
|
-
charges =
|
8
|
+
initial = Regexp.last_match(1) if line && line.match(initial_parser)
|
9
|
+
charges = Regexp.last_match(1) if line && line.match(charges_parser)
|
11
10
|
new(line, initial, charges)
|
12
11
|
end
|
13
12
|
|
14
|
-
def initial_parser
|
15
|
-
|
16
|
-
|
13
|
+
def initial_parser
|
14
|
+
%r{((?:\/OCMT\/)(?<initial>[a-zA-Z]{3}[\d,]{1,15}))}
|
15
|
+
end
|
17
16
|
|
17
|
+
def charges_parser
|
18
|
+
%r{((?:\/CHGS\/)(?<charges>[a-zA-Z]{3}[\d,]{1,15}))}
|
19
|
+
end
|
20
|
+
end
|
18
21
|
|
19
22
|
def initialize(line, initial, charges)
|
20
23
|
self.source = line
|
@@ -44,10 +47,10 @@ module Cmxl
|
|
44
47
|
initial_amount_in_cents: initial_amount_in_cents,
|
45
48
|
initial_currency: initial_currency,
|
46
49
|
charges_in_cents: charges_in_cents,
|
47
|
-
charges_currency: charges_currency
|
50
|
+
charges_currency: charges_currency
|
48
51
|
}
|
49
52
|
end
|
50
|
-
|
53
|
+
alias to_hash to_h
|
51
54
|
end
|
52
55
|
end
|
53
56
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Cmxl
|
2
|
+
module Fields
|
3
|
+
class VmkSummary < Field
|
4
|
+
self.tag = 90
|
5
|
+
self.parser = /(?<entries>\d{,53})(?<currency>\w{3})(?<amount>[\d|,|\.]{1,15})/i
|
6
|
+
|
7
|
+
def credit?
|
8
|
+
modifier == 'C'
|
9
|
+
end
|
10
|
+
|
11
|
+
def debit?
|
12
|
+
modifier == 'D'
|
13
|
+
end
|
14
|
+
|
15
|
+
def entries
|
16
|
+
data['entries'].to_i
|
17
|
+
end
|
18
|
+
|
19
|
+
def amount
|
20
|
+
to_amount(data['amount'])
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_h
|
24
|
+
{
|
25
|
+
type: debit? ? 'debit' : 'credit',
|
26
|
+
entries: entries,
|
27
|
+
amount: amount,
|
28
|
+
currency: currency
|
29
|
+
}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/cmxl/statement.rb
CHANGED
@@ -13,12 +13,12 @@ module Cmxl
|
|
13
13
|
self.source = source
|
14
14
|
self.fields = []
|
15
15
|
self.lines = []
|
16
|
-
|
17
|
-
|
16
|
+
strip_headers! if Cmxl.config[:strip_headers]
|
17
|
+
parse!
|
18
18
|
end
|
19
19
|
|
20
20
|
def transactions
|
21
|
-
|
21
|
+
fields.select { |field| field.is_a?(Fields::Transaction) }
|
22
22
|
end
|
23
23
|
|
24
24
|
# Internal: Parse a single MT940 statement and extract the line data
|
@@ -30,71 +30,86 @@ module Cmxl
|
|
30
30
|
# do not remove line breaks within transaction lines as they are used to determine field details
|
31
31
|
# e.g. :61:-supplementary
|
32
32
|
source.split("\n:").each(&:strip!).each do |line|
|
33
|
-
line = ":#{line}" unless line =~
|
33
|
+
line = ":#{line}" unless line =~ /^:/ # prepend lost : via split
|
34
34
|
|
35
|
-
if line
|
35
|
+
if line =~ /\A:86:/
|
36
36
|
if field = fields.last
|
37
37
|
field.add_meta_data(line)
|
38
38
|
end
|
39
39
|
else
|
40
40
|
field = Field.parse(line)
|
41
|
-
|
41
|
+
fields << field unless field.nil?
|
42
42
|
end
|
43
43
|
end
|
44
44
|
end
|
45
45
|
|
46
46
|
def strip_headers!
|
47
|
-
|
48
|
-
|
49
|
-
|
47
|
+
source.gsub!(/\A.+?(?=^:)/m, '') # beginning: strip every line in the beginning that does not start with a :
|
48
|
+
source.gsub!(/^[^:]+\z/, '') # end: strip every line in the end that does not start with a :
|
49
|
+
source.strip!
|
50
50
|
end
|
51
51
|
|
52
|
-
|
53
52
|
# Public: SHA2 of the provided source
|
54
53
|
# This is an experiment of trying to identify statements. The MT940 itself might not provide a unique identifier
|
55
54
|
#
|
56
55
|
# Returns the SHA2 of the source
|
57
56
|
def sha
|
58
|
-
Digest::SHA2.new.update(
|
57
|
+
Digest::SHA2.new.update(source).to_s
|
59
58
|
end
|
60
59
|
|
61
60
|
def reference
|
62
|
-
|
61
|
+
field(20).reference
|
63
62
|
end
|
64
63
|
|
65
64
|
def generation_date
|
66
|
-
|
65
|
+
field(20).date || field(13).date
|
67
66
|
end
|
68
67
|
|
69
68
|
def account_identification
|
70
|
-
|
69
|
+
field(25)
|
71
70
|
end
|
72
71
|
|
73
72
|
def opening_balance
|
74
|
-
|
73
|
+
field(60, 'F')
|
75
74
|
end
|
76
75
|
|
77
76
|
def opening_or_intermediary_balance
|
78
|
-
|
77
|
+
field(60)
|
79
78
|
end
|
80
79
|
|
81
80
|
def closing_balance
|
82
|
-
|
81
|
+
field(62, 'F')
|
83
82
|
end
|
84
83
|
|
85
84
|
def closing_or_intermediary_balance
|
86
|
-
|
85
|
+
field(62)
|
87
86
|
end
|
88
87
|
|
89
88
|
def available_balance
|
90
|
-
|
89
|
+
field(64)
|
91
90
|
end
|
92
91
|
|
93
92
|
def legal_sequence_number
|
94
|
-
|
93
|
+
field(28).source
|
94
|
+
end
|
95
|
+
|
96
|
+
def vmk_credit_summary
|
97
|
+
field(90, 'C')
|
98
|
+
end
|
99
|
+
|
100
|
+
def vmk_debit_summary
|
101
|
+
field(90, 'D')
|
102
|
+
end
|
103
|
+
|
104
|
+
def mt942?
|
105
|
+
fields.any? { |field| field.is_a? Fields::FloorLimitIndicator }
|
95
106
|
end
|
96
107
|
|
97
108
|
def to_h
|
109
|
+
mt942? ? mt942_hash : mt940_hash
|
110
|
+
end
|
111
|
+
|
112
|
+
def mt940_hash
|
98
113
|
{
|
99
114
|
'reference' => reference,
|
100
115
|
'sha' => sha,
|
@@ -107,9 +122,24 @@ module Cmxl
|
|
107
122
|
'fields' => fields.map(&:to_h)
|
108
123
|
}
|
109
124
|
end
|
125
|
+
|
126
|
+
def mt942_hash
|
127
|
+
{
|
128
|
+
'reference' => reference,
|
129
|
+
'sha' => sha,
|
130
|
+
'generation_date' => generation_date,
|
131
|
+
'account_identification' => account_identification.to_h,
|
132
|
+
'debit_summary' => vmk_debit_summary.to_h,
|
133
|
+
'credit_summary' => vmk_credit_summary.to_h,
|
134
|
+
'transactions' => transactions.map(&:to_h),
|
135
|
+
'fields' => fields.map(&:to_h)
|
136
|
+
}
|
137
|
+
end
|
138
|
+
|
110
139
|
def to_hash
|
111
140
|
to_h
|
112
141
|
end
|
142
|
+
|
113
143
|
def to_json(*args)
|
114
144
|
to_h.to_json(*args)
|
115
145
|
end
|
@@ -120,8 +150,8 @@ module Cmxl
|
|
120
150
|
# Example:
|
121
151
|
# field(20)
|
122
152
|
# field(61,'F')
|
123
|
-
def field(tag, modifier=nil)
|
124
|
-
|
153
|
+
def field(tag, modifier = nil)
|
154
|
+
fields.detect { |field| field.tag == tag.to_s && (modifier.nil? || field.modifier == modifier) }
|
125
155
|
end
|
126
156
|
end
|
127
157
|
end
|
data/lib/cmxl/version.rb
CHANGED