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