mt940_parser 1.5.4 → 1.5.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c10c25af0fbeca4a9f993982b9d6836c93f97a7689aad0437f6d7c296db76627
4
- data.tar.gz: e374fb82f872046d4928c56356e80d26f2aaf2d3ee987ce73ca97b3b8a97f7b6
3
+ metadata.gz: 83c746f0f3871275400a75662a116760798fa3212f443588965c03cb53bfc7a5
4
+ data.tar.gz: f96ef50e6f342890620391ade43d1034a34df62e7320f95502b473abe9d71a3b
5
5
  SHA512:
6
- metadata.gz: 4f2e1879aa0385913ae9d5d1144e55104c94d0eb7195ce5a8cb63a61b5c7121f97371f365fe473c6d30b825d8756102fd4f1f79b5708189d4de4256d993a4fe1
7
- data.tar.gz: e41928d11934d6d2419850bc8792b26f96eda4de933bba63e8caa67034751f211965ff1ca588a8a6d5ded3e8b0acd7937f4ee779bcf337911bd6ea9f02413cb1
6
+ metadata.gz: a9ba3439db4b3e5bb1595ac608c9060a453d0a0b3a3b56a4dd41e5f9eb68d017834932723ad574b0f07f0828935313dd966bec679de825060ca746fec2f84432
7
+ data.tar.gz: 2eacc5076f62817e7b1822426573ae8c2efddc6ecaf8af9d7642cb378490fd9d3d40c481ec7a3483c36077dc191eeac3aab5b05e6f28d0d6953256e379ab450d
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.5.4
1
+ 1.5.5
@@ -3,11 +3,10 @@
3
3
  # the data easier
4
4
  class MT940
5
5
  class CustomerStatementMessage
6
-
7
- attr_reader :account, :statement_lines
6
+ attr_reader :account, :statement_lines, :opening_balance, :closing_balance
8
7
 
9
8
  def self.parse_file(file)
10
- self.parse(File.read(file))
9
+ parse(File.read(file))
11
10
  end
12
11
 
13
12
  def self.parse(data)
@@ -16,12 +15,15 @@ class MT940
16
15
  end
17
16
 
18
17
  def initialize(lines)
19
- @account = lines.find { |line| line.class == MT940::AccountIdentification }
18
+ @account = select_by_type(lines, MT940::AccountIdentification)
19
+ @opening_balance = select_by_type(lines, MT940::AccountBalance)
20
+ @closing_balance = select_by_type(lines, MT940::ClosingBalance)
20
21
  @statement_lines = []
21
22
  lines.each_with_index do |line, i|
22
23
  next unless line.class == MT940::StatementLine
23
- ensure_is_info_line!(lines[i+1])
24
- @statement_lines << StatementLineBundle.new(lines[i], lines[i+1])
24
+
25
+ ensure_is_info_line!(lines[i + 1])
26
+ @statement_lines << StatementLineBundle.new(lines[i], lines[i + 1])
25
27
  end
26
28
  end
27
29
 
@@ -33,36 +35,45 @@ class MT940
33
35
  @account.account_number
34
36
  end
35
37
 
38
+ def signature
39
+ Digest::SHA256.hexdigest(opening_balance.content.to_s + closing_balance.content.to_s)
40
+ end
41
+
36
42
  private
37
43
 
44
+ def select_by_type(lines, line_klass)
45
+ lines.select { |line| line.instance_of?(line_klass) }.first
46
+ end
47
+
38
48
  def ensure_is_info_line!(line)
39
- unless line.is_a? MT940::StatementLineInformation
40
- raise Errors::UnexpectedStructureError,
41
- "Unexpected Structure; expected StatementLineInformation, "\
42
- "but was #{line.class}"
43
- end
49
+ return if line.is_a? MT940::StatementLineInformation
50
+
51
+ raise Errors::UnexpectedStructureError,
52
+ 'Unexpected Structure; expected StatementLineInformation, ' \
53
+ "but was #{line.class}"
44
54
  end
45
55
  end
46
56
 
47
57
  class StatementLineBundle
48
58
  METHOD_MAP = {
49
- :amount => :line,
50
- :funds_code => :line,
51
- :value_date => :line,
52
- :entry_date => :line,
53
- :account_holder => :info,
54
- :details => :info,
55
- :account_number => :info,
56
- :bank_code => :info,
57
- :code => :info,
58
- :transaction_description => :info,
59
+ amount: :line,
60
+ funds_code: :line,
61
+ value_date: :line,
62
+ entry_date: :line,
63
+ account_holder: :info,
64
+ details: :info,
65
+ account_number: :info,
66
+ bank_code: :info,
67
+ code: :info,
68
+ transaction_description: :info
59
69
  }
60
70
 
61
71
  def initialize(statement_line, statement_line_info)
62
- @line, @info = statement_line, statement_line_info
72
+ @line = statement_line
73
+ @info = statement_line_info
63
74
  end
64
75
 
65
- def method_missing(method, *args, &block)
76
+ def method_missing(method, *args, &)
66
77
  super unless METHOD_MAP.has_key?(method)
67
78
  object = instance_variable_get("@#{METHOD_MAP[method.to_sym]}")
68
79
  object.send(method)
data/lib/mt940/version.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  class MT940
2
2
  # MT940 version
3
- VERSION = '1.5.4'
3
+ VERSION = '1.5.5'
4
4
  VERSION_ARRAY = VERSION.split('.').map(&:to_i) # :nodoc:
5
5
  VERSION_MAJOR = VERSION_ARRAY[0] # :nodoc:
6
6
  VERSION_MINOR = VERSION_ARRAY[1] # :nodoc:
data/lib/mt940.rb CHANGED
@@ -4,6 +4,7 @@ require 'mt940/customer_statement_message'
4
4
  require 'bigdecimal'
5
5
  require 'bigdecimal/util'
6
6
  require 'date'
7
+ require 'digest/sha2'
7
8
 
8
9
  class MT940
9
10
  class Field
@@ -13,12 +14,13 @@ class MT940
13
14
  SHORT_DATE = /(\d{2})(\d{2})/
14
15
 
15
16
  class << self
16
-
17
17
  LINE = /^:(\d{2})(\w)?:(.*)$/
18
18
 
19
19
  def for(line)
20
20
  if line.match(LINE)
21
- number, modifier, content = $1, $2, $3
21
+ number = ::Regexp.last_match(1)
22
+ modifier = ::Regexp.last_match(2)
23
+ content = ::Regexp.last_match(3)
22
24
  klass = {
23
25
  '20' => Job,
24
26
  '21' => Reference,
@@ -37,8 +39,8 @@ class MT940
37
39
  klass.new(modifier, content)
38
40
  else
39
41
  raise Errors::WrongLineFormatError,
40
- "Wrong line format does not match #{LINE.inspect}. Got: "\
41
- "#{line.dump[0...80]}#{'[...]' if line.dump.size > 80}"
42
+ "Wrong line format does not match #{LINE.inspect}. Got: " \
43
+ "#{line.dump[0...80]}#{'[...]' if line.dump.size > 80}"
42
44
  end
43
45
  end
44
46
  end
@@ -59,15 +61,14 @@ class MT940
59
61
 
60
62
  def parse_date(date)
61
63
  date.match(DATE)
62
- ::Date.new("20#{$1}".to_i, $2.to_i, $3.to_i)
64
+ ::Date.new("20#{::Regexp.last_match(1)}".to_i, ::Regexp.last_match(2).to_i, ::Regexp.last_match(3).to_i)
63
65
  end
64
66
 
65
67
  def parse_entry_date(raw_entry_date, value_date)
66
68
  raw_entry_date.match(SHORT_DATE)
67
- entry_date = ::Date.new(value_date.year, $1.to_i, $2.to_i)
68
- unless entry_date.year == value_date.year
69
- raise "Unhandled case: value date and entry date are in different years"
70
- end
69
+ entry_date = ::Date.new(value_date.year, ::Regexp.last_match(1).to_i, ::Regexp.last_match(2).to_i)
70
+ raise 'Unhandled case: value date and entry date are in different years' unless entry_date.year == value_date.year
71
+
71
72
  entry_date
72
73
  end
73
74
  end
@@ -88,20 +89,21 @@ class MT940
88
89
  # 25
89
90
  class AccountIdentification < Field
90
91
  attr_reader :account_identifier
91
- CONTENT = /(.{1,35})/ #any 35 chars (35x from the docs)
92
+
93
+ CONTENT = /(.{1,35})/ # any 35 chars (35x from the docs)
92
94
 
93
95
  def parse_content(content)
94
96
  content.match(CONTENT)
95
- @account_identifier = $1
97
+ @account_identifier = ::Regexp.last_match(1)
96
98
  end
97
99
 
98
100
  # fail over to the old Account class
99
- def method_missing(method, *args, &block)
101
+ def method_missing(method, *args, &)
100
102
  @fail_over_implementation ||= Account.new(@modifier, @content)
101
103
  value = @fail_over_implementation.send(method)
102
- warn "[DEPRECATION]:"
104
+ warn '[DEPRECATION]:'
103
105
  warn "You used '#{method}' on the Account/AccountIdentification class"
104
- warn "This field is not part of the MT940 specification but implementation specific"
106
+ warn 'This field is not part of the MT940 specification but implementation specific'
105
107
  warn "Please use the 'account_identifier' and parse yourself."
106
108
 
107
109
  value
@@ -113,11 +115,13 @@ class MT940
113
115
  class Account < Field
114
116
  attr_reader :bank_code, :account_number, :account_currency
115
117
 
116
- CONTENT = /^(.{8,11})\/(\d{0,23})([A-Z]{3})?$/
118
+ CONTENT = %r{^(.{8,11})/(\d{0,23})([A-Z]{3})?$}
117
119
 
118
120
  def parse_content(content)
119
121
  content.match(CONTENT)
120
- @bank_code, @account_number, @account_currency = $1, $2, $3
122
+ @bank_code = ::Regexp.last_match(1)
123
+ @account_number = ::Regexp.last_match(2)
124
+ @account_currency = ::Regexp.last_match(3)
121
125
  end
122
126
  end
123
127
 
@@ -125,14 +129,15 @@ class MT940
125
129
  class Statement < Field
126
130
  attr_reader :number, :sheet
127
131
 
128
- CONTENT = /^(0|(\d{5,5})\/(\d{2,5}))$/
132
+ CONTENT = %r{^(0|(\d{5,5})/(\d{2,5}))$}
129
133
 
130
134
  def parse_content(content)
131
135
  content.match(CONTENT)
132
- if $1 == '0'
136
+ if ::Regexp.last_match(1) == '0'
133
137
  @number = @sheet = 0
134
138
  else
135
- @number, @sheet = $2.to_i, $3.to_i
139
+ @number = ::Regexp.last_match(2).to_i
140
+ @sheet = ::Regexp.last_match(3).to_i
136
141
  end
137
142
  end
138
143
  end
@@ -155,23 +160,23 @@ class MT940
155
160
  end
156
161
 
157
162
  @sign =
158
- case $1
163
+ case ::Regexp.last_match(1)
159
164
  when 'C'
160
165
  :credit
161
166
  when 'D'
162
167
  :debit
163
168
  end
164
169
 
165
- raw_date = $2
166
- @currency = $3
167
- @amount = parse_amount_in_cents($4)
170
+ raw_date = ::Regexp.last_match(2)
171
+ @currency = ::Regexp.last_match(3)
172
+ @amount = parse_amount_in_cents(::Regexp.last_match(4))
168
173
 
169
174
  @date =
170
175
  case raw_date
171
176
  when 'ALT', '0'
172
177
  nil
173
178
  when DATE
174
- ::Date.new("20#{$1}".to_i, $2.to_i, $3.to_i)
179
+ ::Date.new("20#{::Regexp.last_match(1)}".to_i, ::Regexp.last_match(2).to_i, ::Regexp.last_match(3).to_i)
175
180
  end
176
181
  end
177
182
  end
@@ -180,15 +185,15 @@ class MT940
180
185
  class StatementLine < Field
181
186
  attr_reader :date, :entry_date, :funds_code, :amount, :swift_code, :reference, :transaction_description
182
187
 
183
- CONTENT = /^(\d{6})(\d{4})?(C|D|RC|RD)\D?(\d{1,12},\d{0,2})((?:N|F).{3})(NONREF|.{0,16})(?:$|\/\/)(.*)/
188
+ CONTENT = %r{^(\d{6})(\d{4})?(C|D|RC|RD)\D?(\d{1,12},\d{0,2})((?:N|F).{3})(NONREF|.{0,16})(?:$|//)(.*)}
184
189
 
185
190
  def parse_content(content)
186
191
  content.match(CONTENT)
187
192
 
188
- raw_date = $1
189
- raw_entry_date = $2
193
+ raw_date = ::Regexp.last_match(1)
194
+ raw_entry_date = ::Regexp.last_match(2)
190
195
  @funds_code =
191
- case $3
196
+ case ::Regexp.last_match(3)
192
197
  when 'C'
193
198
  :credit
194
199
  when 'D'
@@ -199,10 +204,10 @@ class MT940
199
204
  :return_debit
200
205
  end
201
206
 
202
- @amount = parse_amount_in_cents($4)
203
- @swift_code = $5
204
- @reference = $6
205
- @transaction_description = $7
207
+ @amount = parse_amount_in_cents(::Regexp.last_match(4))
208
+ @swift_code = ::Regexp.last_match(5)
209
+ @reference = ::Regexp.last_match(6)
210
+ @transaction_description = ::Regexp.last_match(7)
206
211
 
207
212
  @date = parse_date(raw_date)
208
213
  @entry_date = parse_entry_date(raw_entry_date, @date) if raw_entry_date
@@ -228,17 +233,17 @@ class MT940
228
233
  # 86
229
234
  class StatementLineInformation < Field
230
235
  attr_reader :code, :transaction_description, :prima_nota, :details, :bank_code, :account_number,
231
- :account_holder, :text_key_extension, :not_implemented_fields
236
+ :account_holder, :text_key_extension, :not_implemented_fields
232
237
 
233
238
  def parse_content(content)
234
239
  content.match(/^(\d{3})((.).*)$/)
235
- @code = $1.to_i
240
+ @code = ::Regexp.last_match(1).to_i
236
241
 
237
242
  details = []
238
243
  account_holder = []
239
244
 
240
- if seperator = $3
241
- sub_fields = $2.scan(
245
+ if seperator = ::Regexp.last_match(3)
246
+ sub_fields = ::Regexp.last_match(2).scan(
242
247
  /#{Regexp.escape(seperator)}(\d{2})([^#{Regexp.escape(seperator)}]*)/
243
248
  )
244
249
 
@@ -260,10 +265,8 @@ class MT940
260
265
  @text_key_extension = content
261
266
  else
262
267
  @not_implemented_fields ||= []
263
- @not_implemented_fields << [ code, content ]
264
- if $DEBUG
265
- warn "code not implemented: code:#{code} content: #{content.inspect}"
266
- end
268
+ @not_implemented_fields << [code, content]
269
+ warn "code not implemented: code:#{code} content: #{content.inspect}" if $DEBUG
267
270
  end
268
271
  end
269
272
  end
@@ -287,8 +290,7 @@ class MT940
287
290
 
288
291
  def parse_sheet(sheet)
289
292
  lines = sheet.split("\r\n")
290
- fields = lines.reject { |line| line.empty? }.map { |line| Field.for(line) }
291
- fields
293
+ lines.reject { |line| line.empty? }.map { |line| Field.for(line) }
292
294
  end
293
295
  end
294
296
  end
data/mt940_parser.gemspec CHANGED
@@ -1,14 +1,14 @@
1
1
  # -*- encoding: utf-8 -*-
2
- # stub: mt940_parser 1.5.4 ruby lib
2
+ # stub: mt940_parser 1.5.5 ruby lib
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "mt940_parser".freeze
6
- s.version = "1.5.4"
6
+ s.version = "1.5.5"
7
7
 
8
8
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
9
9
  s.require_paths = ["lib".freeze]
10
10
  s.authors = ["Thies C. Arntzen".freeze, "Phillip Oertel".freeze]
11
- s.date = "2022-05-27"
11
+ s.date = "2025-05-21"
12
12
  s.description = "Ruby library that parses account statements in the SWIFT MT940 format.".freeze
13
13
  s.email = "developers@betterplace.org".freeze
14
14
  s.extra_rdoc_files = ["README.md".freeze, "lib/mt940.rb".freeze, "lib/mt940/customer_statement_message.rb".freeze, "lib/mt940/errors.rb".freeze, "lib/mt940/version.rb".freeze, "lib/mt940_parser.rb".freeze]
@@ -1,22 +1,20 @@
1
1
  require_relative 'test_helper'
2
2
 
3
-
4
3
  # $DEBUG = true
5
4
  class TestCustomerStatementMessage < Test::Unit::TestCase
6
-
7
5
  def setup
8
- file = File.dirname(__FILE__) + "/fixtures/sepa_snippet.txt"
6
+ file = File.dirname(__FILE__) + '/fixtures/sepa_snippet.txt'
9
7
  messages = MT940::CustomerStatementMessage.parse_file(file)
10
8
  @message = messages.first
11
9
  @message_2 = messages.last
12
10
  end
13
11
 
14
12
  def test_it_should_know_the_bank_code
15
- assert_equal "50880050", @message.bank_code
13
+ assert_equal '50880050', @message.bank_code
16
14
  end
17
15
 
18
16
  def test_it_should_know_the_account_number
19
- assert_equal "0194787400888", @message.account_number
17
+ assert_equal '0194787400888', @message.account_number
20
18
  end
21
19
 
22
20
  def test_it_should_have_an_account_identification
@@ -30,7 +28,7 @@ class TestCustomerStatementMessage < Test::Unit::TestCase
30
28
 
31
29
  def test_statement_lines_should_have_amount_info_credit
32
30
  line = @message.statement_lines.first
33
- assert_equal 5099005, line.amount
31
+ assert_equal 5_099_005, line.amount
34
32
  assert_equal :credit, line.funds_code
35
33
  end
36
34
 
@@ -47,12 +45,12 @@ class TestCustomerStatementMessage < Test::Unit::TestCase
47
45
 
48
46
  def test_statement_lines_info_should_have_bank_code
49
47
  line = @message.statement_lines.first
50
- assert_equal "DRESDEFF508", line.bank_code
48
+ assert_equal 'DRESDEFF508', line.bank_code
51
49
  end
52
50
 
53
51
  def test_statement_lines_info_should_have_account_number
54
52
  line = @message.statement_lines.first
55
- assert_equal "DE14508800500194785000", line.account_number
53
+ assert_equal 'DE14508800500194785000', line.account_number
56
54
  end
57
55
 
58
56
  def test_statement_lines_should_have_details
@@ -62,32 +60,45 @@ class TestCustomerStatementMessage < Test::Unit::TestCase
62
60
 
63
61
  def test_statement_lines_should_have_an_entry_date
64
62
  line = @message.statement_lines.first
65
- assert_equal Date.parse("2007-09-04"), line.entry_date
63
+ assert_equal Date.parse('2007-09-04'), line.entry_date
66
64
  end
67
65
 
68
66
  def test_statement_lines_should_have_a_value_date
69
67
  line = @message.statement_lines.first
70
- assert_equal Date.parse("2007-09-07"), line.value_date
68
+ assert_equal Date.parse('2007-09-07'), line.value_date
71
69
  end
72
70
 
73
71
  def test_parsing_the_file_should_return_two_message_objects
74
- file = File.dirname(__FILE__) + "/fixtures/sepa_snippet.txt"
72
+ file = File.dirname(__FILE__) + '/fixtures/sepa_snippet.txt'
75
73
  messages = MT940::CustomerStatementMessage.parse_file(file)
76
74
  assert_equal 2, messages.size
77
- assert_equal "0194787400888", messages[0].account_number
78
- assert_equal "0194791600888", messages[1].account_number
75
+ assert_equal '0194787400888', messages[0].account_number
76
+ assert_equal '0194791600888', messages[1].account_number
79
77
  end
80
78
 
81
79
  def test_parsing_a_file_with_broken_structure_should_raise_an_exception
82
- file = File.dirname(__FILE__) + "/fixtures/sepa_snippet_broken.txt"
80
+ file = File.dirname(__FILE__) + '/fixtures/sepa_snippet_broken.txt'
83
81
  assert_raise(MT940::Errors::UnexpectedStructureError) do
84
82
  MT940::CustomerStatementMessage.parse_file(file)
85
83
  end
86
84
  end
87
85
 
88
86
  def test_should_raise_method_missing_when_asking_statement_lines_for_unknown_stuff
89
- file = File.dirname(__FILE__) + "/fixtures/sepa_snippet.txt"
87
+ file = File.dirname(__FILE__) + '/fixtures/sepa_snippet.txt'
88
+ message = MT940::CustomerStatementMessage.parse_file(file).first
89
+ assert_raise(NoMethodError) { message.statement_lines.first.foo }
90
+ end
91
+
92
+ def test_should_parse_opening_and_closing_saldo
93
+ file = File.dirname(__FILE__) + '/fixtures/sepa_snippet.txt'
94
+ message = MT940::CustomerStatementMessage.parse_file(file).first
95
+ assert_equal(message.opening_balance.amount, 76_665_649)
96
+ assert_equal(message.closing_balance.amount, 112_525_040)
97
+ end
98
+
99
+ def test_should_have_signature
100
+ file = File.dirname(__FILE__) + '/fixtures/sepa_snippet.txt'
90
101
  message = MT940::CustomerStatementMessage.parse_file(file).first
91
- assert_raise(NoMethodError) { message.statement_lines.first.foo }
102
+ assert_equal(message.signature, 'b16439633c7c52fed8e28f717f364e7ec1936f123aa56ab66b15662093cb60ca')
92
103
  end
93
104
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mt940_parser
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.4
4
+ version: 1.5.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thies C. Arntzen
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2022-05-27 00:00:00.000000000 Z
12
+ date: 2025-05-21 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: gem_hadar