cmxl 1.2.0 → 1.3.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.
- 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