cmxl 1.2.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +5 -2
  3. data/CHANGELOG.mdown +28 -18
  4. data/README.md +45 -26
  5. data/cmxl.gemspec +15 -16
  6. data/lib/cmxl.rb +9 -9
  7. data/lib/cmxl/field.rb +25 -18
  8. data/lib/cmxl/fields/account_balance.rb +7 -7
  9. data/lib/cmxl/fields/account_identification.rb +1 -1
  10. data/lib/cmxl/fields/floor_limit_indicator.rb +20 -0
  11. data/lib/cmxl/fields/reference.rb +3 -4
  12. data/lib/cmxl/fields/statement_details.rb +13 -13
  13. data/lib/cmxl/fields/transaction.rb +10 -1
  14. data/lib/cmxl/fields/transaction_supplementary.rb +11 -8
  15. data/lib/cmxl/fields/vmk_summary.rb +33 -0
  16. data/lib/cmxl/statement.rb +52 -22
  17. data/lib/cmxl/version.rb +1 -1
  18. data/spec/field_spec.rb +8 -9
  19. data/spec/fields/account_balance_spec.rb +18 -18
  20. data/spec/fields/account_identification_spec.rb +2 -6
  21. data/spec/fields/available_balance_spec.rb +1 -3
  22. data/spec/fields/closing_balance_spec.rb +2 -4
  23. data/spec/fields/entry_date_spec.rb +1 -1
  24. data/spec/fields/floor_limit_indicator_spec.rb +30 -0
  25. data/spec/fields/reference_spec.rb +2 -3
  26. data/spec/fields/statement_details_spec.rb +61 -56
  27. data/spec/fields/statement_number_spec.rb +0 -2
  28. data/spec/fields/transaction_spec.rb +4 -4
  29. data/spec/fields/transaction_supplementary_spec.rb +4 -4
  30. data/spec/fields/unknown_spec.rb +1 -2
  31. data/spec/fields/vmk_summary_spec.rb +23 -0
  32. data/spec/fixtures/lines/floor_limit_indicator_both.txt +1 -0
  33. data/spec/fixtures/lines/floor_limit_indicator_credit.txt +1 -0
  34. data/spec/fixtures/lines/floor_limit_indicator_debit.txt +1 -0
  35. data/spec/fixtures/lines/vmk_summary_credit.txt +1 -0
  36. data/spec/fixtures/lines/vmk_summary_debit.txt +1 -0
  37. data/spec/fixtures/mt942.txt +9 -0
  38. data/spec/fixtures/statement-details-mt942.txt +4 -0
  39. data/spec/mt940_parsing_spec.rb +13 -14
  40. data/spec/spec_helper.rb +3 -6
  41. data/spec/statement_spec.rb +119 -99
  42. data/spec/support/fixtures.rb +1 -1
  43. 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(self.data['date'])
8
+ to_date(data['date'])
9
9
  end
10
10
 
11
11
  def credit?
12
- self.data['funds_code'].to_s.upcase == 'C'
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(self.data['amount'])
20
+ to_amount(data['amount'])
21
21
  end
22
22
 
23
23
  def sign
24
- self.credit? ? 1 : -1
24
+ credit? ? 1 : -1
25
25
  end
26
26
 
27
27
  def amount_in_cents
28
- to_amount_in_cents(self.data['amount'])
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
@@ -5,7 +5,7 @@ module Cmxl
5
5
  self.parser = /(?<bank_code>\w{8,11})\/(?<account_number>\d{0,23})(?<currency>[A-Z]{3})?|(?<country>[a-zA-Z]{2})(?<ban>\d{11,36})/i
6
6
 
7
7
  def iban
8
- "#{self.country}#{self.ban}"
8
+ "#{country}#{ban}"
9
9
  end
10
10
  end
11
11
  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
- self.source
8
+ source
9
9
  end
10
10
 
11
11
  def date
12
- to_date(self.data['date'])
12
+ to_date(data['date'])
13
13
  end
14
14
 
15
15
  def to_h
16
- super.merge({'date' => date, 'reference' => source})
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.gsub(/\n/, '')
10
+ super line.delete("\n")
11
11
  end
12
12
  end
13
13
 
14
14
  def sub_fields
15
- @sub_fields ||= if self.data['details'] =~ /#{Regexp.escape(self.data['seperator'])}(\d{2})/
16
- Hash[self.data['details'].scan(/#{Regexp.escape(self.data['seperator'])}(\d{2})([^#{Regexp.escape(self.data['seperator'])}]*)/)]
17
- else
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
- self.sub_fields['00'] || self.data['details']
23
+ sub_fields['00'] || data['details']
24
24
  end
25
25
 
26
26
  def information
27
- info = (20..29).to_a.collect {|i| self.sub_fields[i.to_s] }.join('')
28
- info.empty? ? self.description : info
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 self.information =~ /([A-Z]{4})\+/
32
+ if information =~ /([A-Z]{4})\+/
33
33
  Hash[
34
- *self.information.split(/([A-Z]{4})\+/)[1..-1].tap {|info| info << "" if info.size.odd? }
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
- self.sub_fields['30']
42
+ sub_fields['30']
43
43
  end
44
44
 
45
45
  def name
46
- [self.sub_fields['32'], self.sub_fields['33']].compact.join(" ")
46
+ [sub_fields['32'], sub_fields['33']].compact.join(' ')
47
47
  end
48
48
 
49
49
  def iban
50
- self.sub_fields['38'] || self.sub_fields['31']
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 = $1 if line && line.match(initial_parser)
10
- charges = $1 if line && line.match(charges_parser)
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; %r{((?:\/OCMT\/)(?<initial>[a-zA-Z]{3}[\d,]{1,15}))} end
15
- def charges_parser; %r{((?:\/CHGS\/)(?<charges>[a-zA-Z]{3}[\d,]{1,15}))} end
16
- end
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
- alias_method :to_hash, :to_h
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
@@ -13,12 +13,12 @@ module Cmxl
13
13
  self.source = source
14
14
  self.fields = []
15
15
  self.lines = []
16
- self.strip_headers! if Cmxl.config[:strip_headers]
17
- self.parse!
16
+ strip_headers! if Cmxl.config[:strip_headers]
17
+ parse!
18
18
  end
19
19
 
20
20
  def transactions
21
- self.fields.select { |field| field.kind_of?(Fields::Transaction) }
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 =~ %r{^:} # prepend lost : via split
33
+ line = ":#{line}" unless line =~ /^:/ # prepend lost : via split
34
34
 
35
- if line.match(/\A:86:/)
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
- self.fields << field unless field.nil?
41
+ fields << field unless field.nil?
42
42
  end
43
43
  end
44
44
  end
45
45
 
46
46
  def strip_headers!
47
- self.source.gsub!(/\A.+?(?=^:)/m, '') # beginning: strip every line in the beginning that does not start with a :
48
- self.source.gsub!(/^[^:]+\z/, '') # end: strip every line in the end that does not start with a :
49
- self.source.strip!
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(self.source).to_s
57
+ Digest::SHA2.new.update(source).to_s
59
58
  end
60
59
 
61
60
  def reference
62
- self.field(20).reference
61
+ field(20).reference
63
62
  end
64
63
 
65
64
  def generation_date
66
- self.field(20).date || self.field(13).date
65
+ field(20).date || field(13).date
67
66
  end
68
67
 
69
68
  def account_identification
70
- self.field(25)
69
+ field(25)
71
70
  end
72
71
 
73
72
  def opening_balance
74
- self.field(60, 'F')
73
+ field(60, 'F')
75
74
  end
76
75
 
77
76
  def opening_or_intermediary_balance
78
- self.field(60)
77
+ field(60)
79
78
  end
80
79
 
81
80
  def closing_balance
82
- self.field(62, 'F')
81
+ field(62, 'F')
83
82
  end
84
83
 
85
84
  def closing_or_intermediary_balance
86
- self.field(62)
85
+ field(62)
87
86
  end
88
87
 
89
88
  def available_balance
90
- self.field(64)
89
+ field(64)
91
90
  end
92
91
 
93
92
  def legal_sequence_number
94
- self.field(28).source
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
- self.fields.detect {|field| field.tag == tag.to_s && (modifier.nil? || field.modifier == modifier) }
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
@@ -1,3 +1,3 @@
1
1
  module Cmxl
2
- VERSION = '1.2.0'
2
+ VERSION = '1.3.0'.freeze
3
3
  end