codabel 1.0.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 +7 -0
  2. data/Gemfile +2 -0
  3. data/LICENSE.md +20 -0
  4. data/README.md +72 -0
  5. data/Rakefile +11 -0
  6. data/lib/codabel/column.rb +35 -0
  7. data/lib/codabel/error.rb +5 -0
  8. data/lib/codabel/file.rb +49 -0
  9. data/lib/codabel/model/account.rb +45 -0
  10. data/lib/codabel/model.rb +5 -0
  11. data/lib/codabel/record/header.rb +23 -0
  12. data/lib/codabel/record/movement.rb +153 -0
  13. data/lib/codabel/record/movement21.rb +37 -0
  14. data/lib/codabel/record/movement22.rb +24 -0
  15. data/lib/codabel/record/movement23.rb +19 -0
  16. data/lib/codabel/record/movement31.rb +20 -0
  17. data/lib/codabel/record/movement32.rb +17 -0
  18. data/lib/codabel/record/movement33.rb +17 -0
  19. data/lib/codabel/record/new_balance.rb +27 -0
  20. data/lib/codabel/record/old_balance.rb +20 -0
  21. data/lib/codabel/record/trailer.rb +73 -0
  22. data/lib/codabel/record.rb +107 -0
  23. data/lib/codabel/type/account_and_currency.rb +31 -0
  24. data/lib/codabel/type/account_description.rb +10 -0
  25. data/lib/codabel/type/account_structure.rb +19 -0
  26. data/lib/codabel/type/amount.rb +11 -0
  27. data/lib/codabel/type/amount_sign.rb +9 -0
  28. data/lib/codabel/type/an.rb +12 -0
  29. data/lib/codabel/type/blank.rb +9 -0
  30. data/lib/codabel/type/communication.rb +37 -0
  31. data/lib/codabel/type/communication_type.rb +43 -0
  32. data/lib/codabel/type/date.rb +14 -0
  33. data/lib/codabel/type/duplicate.rb +10 -0
  34. data/lib/codabel/type/flag.rb +15 -0
  35. data/lib/codabel/type/holder.rb +15 -0
  36. data/lib/codabel/type/n.rb +12 -0
  37. data/lib/codabel/type/return_transaction_type.rb +29 -0
  38. data/lib/codabel/type.rb +28 -0
  39. data/lib/codabel/version.rb +3 -0
  40. data/lib/codabel.rb +11 -0
  41. data/tasks/gem.rake +16 -0
  42. data/tasks/test.rake +17 -0
  43. metadata +111 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d9d1a4c989682a9479310e83a0e81e22f1d8a22b0d8d7b9f08dc348a12264f2d
4
+ data.tar.gz: 91c1bdbd716b4e27d1091a84bcc95be97910c4d79306ded78f276f1f6b0656c4
5
+ SHA512:
6
+ metadata.gz: c6907d15a4624653e8522f053d7daa4ff6c68c4d25a047876fab664330c1f1b03294dec005d5c604ff1f1a4e5c4de48b0bdd77f3725026215aa39fd82ea40f60
7
+ data.tar.gz: 5e5ed839bd40d27d7c7c495107aef8bd7c5b4e51980c843934e8509d45c3731de430e00088712360c9ca66a7f756a5e5a4de9e2dbd7bd18d517489a9dadb7bcc
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "https://rubygems.org"
2
+ gemspec
data/LICENSE.md ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2021 - Flexio SRL
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,72 @@
1
+ # Codabel - Generate CODA files with ease
2
+
3
+ This gem allows generating CODA files (belgian financial format) from structured
4
+ data. We currently support version 2.6 of the Febelfin specification:
5
+
6
+ - https://www.febelfin.be/sites/default/files/2019-04/standard-coda-2.6-en.pdf
7
+
8
+ ## Features
9
+
10
+ * Support for CODA records 0, 1, 2.1, 2.2, 2.3, 3.1, 3.2, 3.3, 8 and 9.
11
+ * Easy CODA generation from a high-level data model
12
+ * Access for fine-grained details through detailed record data
13
+ * Automatical split of mouvements to records 2.x and 3.x
14
+ * Automatic generation of next and link codes
15
+ * Automatic generation of trailer record details
16
+ * Basic check of balances and trailer record (debit, credit, record count)
17
+
18
+ ### Limitations
19
+
20
+ * No support for free information records (4)
21
+ * No high-level data model for transaction codes (record 2.1)
22
+ * No high-level data model for R-transaction and reason (record 2.2)
23
+ * No high-level data model for purpose and category purpose (record 2.2)
24
+
25
+ ## Example
26
+
27
+ ```ruby
28
+ today = Date.parse('2021-11-18')
29
+ file = Codabel::File.new
30
+ file << Codabel::Record.header(
31
+ creation_date: today
32
+ )
33
+ file << Codabel::Record.old_balance(
34
+ balance_date: today,
35
+ balance: 1765
36
+ )
37
+ file << Codabel::Record.movement(
38
+ amount: 107980, # amounts are always in cents
39
+ entry_date: today,
40
+ value_date: today,
41
+ communication: {
42
+ structured: '121204102125'
43
+ }
44
+ )
45
+ file << Codabel::Record.movement(
46
+ amount: -6789,
47
+ entry_date: today,
48
+ value_date: today,
49
+ communication: {
50
+ unstructured: 'Buying flowers'
51
+ }
52
+ )
53
+ file << Codabel::Record.new_balance(
54
+ balance_date: today,
55
+ balance: 1765 + 107980 - 6789
56
+ )
57
+ file << Codabel::Record.trailer
58
+ puts file.to_coda
59
+ ```
60
+
61
+ ## Licence
62
+
63
+ Codabel is distributed by Flexio (https://flexio.app) under a MIT licence
64
+ (see LICENCE.md)
65
+
66
+ ## Contribute
67
+
68
+ * Fork the library
69
+ * Add the features that you want together with the unit and integration tests
70
+ * Please double check that you don't introduce a broken API without reason
71
+ * Open a pull request and relax!
72
+ * Don't hesitate to ping the maintainer by email (bernard, you know, at flexio.app)
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ #
2
+ # Install all tasks found in tasks folder
3
+ #
4
+ # See .rake files there for complete documentation.
5
+ #
6
+ Dir["tasks/*.rake"].each do |taskfile|
7
+ load taskfile
8
+ end
9
+
10
+ # We run tests by default
11
+ task :default => :test
@@ -0,0 +1,35 @@
1
+ module Codabel
2
+ class Column
3
+ def initialize(range, path, type, options)
4
+ @range = range
5
+ @path = Array(path)
6
+ @type = type
7
+ @options = options
8
+ end
9
+ attr_reader :range
10
+ attr_reader :path
11
+ attr_reader :type
12
+ attr_reader :options
13
+
14
+ def length
15
+ range.size
16
+ end
17
+
18
+ def to_coda(record)
19
+ value = record.data.dig(*path) unless path.empty?
20
+ value = options[:default] if value.nil?
21
+ type.to_coda(value, length)
22
+ end
23
+
24
+ def path_starts_with?(prefix)
25
+ prefix = Array(prefix)
26
+ @path[0...prefix.size] == prefix
27
+ end
28
+
29
+ def specifics?(data)
30
+ return false if path.empty?
31
+
32
+ !!data.dig(*path)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,5 @@
1
+ module Codabel
2
+ class Error < StandardError; end
3
+ class ValidationError < Error; end
4
+ class TypeError < Error; end
5
+ end
@@ -0,0 +1,49 @@
1
+ module Codabel
2
+ class File
3
+ def initialize(records = [])
4
+ @records = records
5
+ end
6
+ attr_reader :records
7
+
8
+ def <<(record)
9
+ @records << record
10
+ end
11
+
12
+ def find_records(type)
13
+ @records.filter { |record| record.is_a?(type) }
14
+ end
15
+
16
+ def find_record(type)
17
+ find_records(type).first
18
+ end
19
+
20
+ def validate!
21
+ with_actual_records._validate!
22
+ end
23
+
24
+ def to_coda
25
+ with_actual_records.auto_enrich._validate!._to_coda
26
+ end
27
+
28
+ protected
29
+
30
+ def with_actual_records
31
+ File.new(@records.map { |record| record.actual_records(self) }.flatten)
32
+ end
33
+
34
+ def auto_enrich
35
+ File.new(@records.map { |record| record.auto_enrich(self) }.flatten)
36
+ end
37
+
38
+ def _validate!
39
+ @records.each { |record| record.validate!(self) }
40
+ self
41
+ end
42
+
43
+ def _to_coda
44
+ @records.each_with_object('') do |record, memo|
45
+ memo << record.to_coda << "\n"
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,45 @@
1
+ module Codabel
2
+ class Model
3
+ class Account < Model
4
+ BELGIAN_BBAN = :belgian_bban
5
+ FOREIGN_BBAN = :foreign_bban
6
+ BELGIAN_IBAN = :belgian_iban
7
+ FOREIGN_IBAN = :foreign_iban
8
+
9
+ def self.dress(value)
10
+ case value
11
+ when Account then value
12
+ when String then new(number: value)
13
+ when Hash then new(value)
14
+ else
15
+ raise ArgumentError, "Unable to dress `#{value}` as an Account"
16
+ end
17
+ end
18
+
19
+ def currency
20
+ @currency ||= (super || 'EUR').to_s.gsub(/\s/, '')
21
+ end
22
+
23
+ def number
24
+ @number ||= super.to_s.gsub(/\s/, '')
25
+ end
26
+
27
+ def structure
28
+ @structure ||= super || infer_structure
29
+ end
30
+
31
+ def infer_structure
32
+ case number.to_s.strip
33
+ when /^BE\d{14}$/
34
+ BELGIAN_IBAN
35
+ when /^[A-Z]{2}\d{2}\d{12,30}/
36
+ FOREIGN_IBAN
37
+ when /^\d{12}$/
38
+ BELGIAN_BBAN
39
+ else
40
+ FOREIGN_BBAN
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,5 @@
1
+ module Codabel
2
+ class Model < OpenStruct
3
+ end
4
+ end
5
+ require_relative 'model/account'
@@ -0,0 +1,23 @@
1
+ module Codabel
2
+ class Record
3
+ class Header < Record
4
+ column 1..1, nil, Type::N, default: 0
5
+ column 2..5, nil, Type::N, default: 0
6
+ column 6..11, :creation_date, Type::Date, default: Date.today
7
+ column 12..14, :bank_identifier, Type::N, default: 0
8
+ column 15..16, :application_code, Type::N, default: 5
9
+ column 17..17, :duplicate, Type::Duplicate, default: false
10
+ column 18..24, nil, Type::Blank
11
+ column 25..34, :file_reference, Type::AN, default: ''
12
+ column 35..60, :addressee_name, Type::AN, default: ''
13
+ column 61..71, [:bank, :bic], Type::AN, default: ''
14
+ column 72..82, :holder_identifier, Type::Holder, default: ''
15
+ column 83..83, nil, Type::Blank
16
+ column 84..88, :distinct_app_code, Type::N, default: 0
17
+ column 89..104, :transaction_reference, Type::AN, default: ''
18
+ column 105..120, :related_reference, Type::AN, default: ''
19
+ column 121..127, nil, Type::Blank
20
+ column 128..128, :version_code, Type::N, default: 2
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,153 @@
1
+ module Codabel
2
+ class Record
3
+ class Movement < Record
4
+ def to_coda
5
+ actual_movements.map(&:to_coda).join("\n")
6
+ end
7
+
8
+ def actual_records(_file)
9
+ actual_movements
10
+ end
11
+
12
+ private
13
+
14
+ def actual_movements
15
+ movements = []
16
+ movements << Movement21.new(data_21)
17
+ movements << Movement22.new(data_22) if data_22_required?
18
+ movements << Movement23.new(data_23) if data_23_required?
19
+ movements << Movement31.new(data_31) if data_31_required?
20
+ movements << Movement32.new(data_32) if data_32_required?
21
+ movements << Movement33.new(data_33) if data_33_required?
22
+ movements
23
+ end
24
+
25
+ def data_21
26
+ next_code = data_22_required? || data_23_required?
27
+ data.merge(
28
+ communication: communication(2, 0, Movement21::COMM_LENGTH),
29
+ next_code: next_code,
30
+ link_code: !next_code && data_31_required?
31
+ ).compact
32
+ end
33
+
34
+ def data_22_required?
35
+ return true if Movement22.required?(data, ignore: Movement22::SHARED)
36
+
37
+ !!communication_22
38
+ end
39
+
40
+ def communication_22
41
+ communication(2, Movement21::COMM_LENGTH, Movement22::COMM_LENGTH)
42
+ end
43
+
44
+ def data_22
45
+ next_code = data_23_required?
46
+ data.merge(
47
+ communication: communication_22,
48
+ next_code: next_code,
49
+ link_code: !next_code && data_31_required?
50
+ )
51
+ end
52
+
53
+ def data_23_required?
54
+ return true if Movement23.required?(data, ignore: Movement23::SHARED)
55
+
56
+ !!communication_23
57
+ end
58
+
59
+ def communication_23
60
+ communication(2, Movement21::COMM_LENGTH + Movement22::COMM_LENGTH, Movement23::COMM_LENGTH)
61
+ end
62
+
63
+ def data_23
64
+ data.merge(
65
+ communication: communication_23,
66
+ link_code: data_31_required?
67
+ )
68
+ end
69
+
70
+ def data_31_required?
71
+ return false unless normalized_communication[:structured]
72
+ return false unless normalized_communication[:unstructured]
73
+
74
+ !normalized_communication[:unstructured].empty?
75
+ end
76
+
77
+ def communication_31
78
+ communication(3, 0, Movement31::COMM_LENGTH)
79
+ end
80
+
81
+ def data_31
82
+ data.merge(
83
+ communication: communication_31,
84
+ next_code: data_32_required? || data_33_required?
85
+ )
86
+ end
87
+
88
+ def data_32_required?
89
+ return false unless normalized_communication[:structured]
90
+ return false unless normalized_communication[:unstructured]
91
+
92
+ !!communication_32
93
+ end
94
+
95
+ def communication_32
96
+ communication(3, Movement31::COMM_LENGTH, Movement32::COMM_LENGTH)
97
+ end
98
+
99
+ def data_32
100
+ data.merge(
101
+ communication: communication_32,
102
+ next_code: data_33_required?
103
+ )
104
+ end
105
+
106
+ def data_33_required?
107
+ return false unless normalized_communication[:structured]
108
+ return false unless normalized_communication[:unstructured]
109
+
110
+ !!communication_33
111
+ end
112
+
113
+ def communication_33
114
+ communication(3, Movement31::COMM_LENGTH + Movement32::COMM_LENGTH, Movement33::COMM_LENGTH)
115
+ end
116
+
117
+ def data_33
118
+ data.merge(communication: communication_33)
119
+ end
120
+
121
+ def communication(level, from, max_length)
122
+ communication = normalized_communication
123
+ which = if level == 2
124
+ communication[:structured] ? :structured : :unstructured
125
+ else
126
+ :unstructured
127
+ end
128
+ communication = communication.slice(which)
129
+ communication[which] = communication[which][from...(from + max_length)].to_s
130
+ communication[which] = nil if communication[which].empty?
131
+ communication = communication.compact
132
+ communication.empty? ? nil : communication
133
+ end
134
+
135
+ def normalized_communication
136
+ @normalized_communication ||= begin
137
+ communication = data[:communication]
138
+ case communication
139
+ when NilClass
140
+ { unstructured: '' }
141
+ when String
142
+ { unstructured: communication }
143
+ when Hash
144
+ communication = { unstructured: '' } if communication.empty?
145
+ communication
146
+ else
147
+ check!(false, "Unexpected communication #{communication}")
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,37 @@
1
+ module Codabel
2
+ class Record
3
+ class Movement21 < Record
4
+ COMM_LENGTH = 1 + (115 - 63)
5
+
6
+ column 1..1, nil, Type::N, default: 2
7
+ column 2..2, nil, Type::N, default: 1
8
+ column 3..6, :sequence_number, Type::N, default: 1
9
+ column 7..10, :detail_number, Type::N, default: 0
10
+ column 11..31, :bank_reference, Type::AN, default: ''
11
+ column 32..32, :amount, Type::AmountSign, default: 0
12
+ column 33..47, :amount, Type::Amount, default: 0
13
+ column 48..53, :value_date, Type::Date, default: nil
14
+ column 54..61, :transaction_code, Type::N, default: 0
15
+ column 62..62, :communication, Type::CommunicationType, default: ''
16
+ column 63..115, :communication, Type::Communication, default: ''
17
+ column 116..121, :entry_date, Type::Date, default: Date.today
18
+ column 122..124, :sequence_number_paper, Type::N, default: 0
19
+ column 125..125, :globalisation_code, Type::N, default: 0
20
+ column 126..126, :next_code, Type::Flag, default: false
21
+ column 127..127, nil, Type::Blank, default: ''
22
+ column 128..128, :link_code, Type::Flag, default: false
23
+
24
+ def amount
25
+ data[:amount] || 0
26
+ end
27
+
28
+ def debit_amount
29
+ data[:amount] <= 0 ? data[:amount] : 0
30
+ end
31
+
32
+ def credit_amount
33
+ data[:amount] > 0 ? data[:amount] : 0
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,24 @@
1
+ module Codabel
2
+ class Record
3
+ class Movement22 < Record
4
+ COMM_LENGTH = 1 + (63 - 11)
5
+ SHARED = %i[sequence_number detail_number communication next_code link_code].freeze
6
+
7
+ column 1..1, nil, Type::N, default: 2
8
+ column 2..2, nil, Type::N, default: 2
9
+ column 3..6, :sequence_number, Type::N, default: 1
10
+ column 7..10, :detail_number, Type::N, default: 0
11
+ column 11..63, :communication, Type::Communication, default: ''
12
+ column 64..98, %i[counterparty reference], Type::AN, default: ''
13
+ column 99..109, %i[counterparty bank bic], Type::AN, default: ''
14
+ column 110..112, nil, Type::Blank, default: ''
15
+ column 113..113, :return_transaction_type, Type::ReturnTransactionType, default: ''
16
+ column 114..117, :reason_return_code, Type::AN, default: nil
17
+ column 118..121, :purpose_category, Type::AN, default: ''
18
+ column 122..125, :purpose, Type::AN, default: ''
19
+ column 126..126, :next_code, Type::Flag, default: false
20
+ column 127..127, nil, Type::Blank, default: ''
21
+ column 128..128, :link_code, Type::Flag, default: false
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,19 @@
1
+ module Codabel
2
+ class Record
3
+ class Movement23 < Record
4
+ COMM_LENGTH = 1 + (125 - 83)
5
+ SHARED = %i[sequence_number detail_number communication link_code].freeze
6
+
7
+ column 1..1, nil, Type::N, default: 2
8
+ column 2..2, nil, Type::N, default: 3
9
+ column 3..6, :sequence_number, Type::N, default: 1
10
+ column 7..10, :detail_number, Type::N, default: 0
11
+ column 11..47, %i[counterparty account], Type::AccountAndCurrency, default: ''
12
+ column 48..82, %i[counterparty name], Type::AN, default: ''
13
+ column 83..125, :communication, Type::Communication, default: ''
14
+ column 126..126, nil, Type::N, default: 0
15
+ column 127..127, nil, Type::Blank, default: ''
16
+ column 128..128, :link_code, Type::Flag, default: false
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,20 @@
1
+ module Codabel
2
+ class Record
3
+ class Movement31 < Record
4
+ COMM_LENGTH = 1 + (113 - 41)
5
+
6
+ column 1..1, nil, Type::N, default: 3
7
+ column 2..2, nil, Type::N, default: 1
8
+ column 3..6, :sequence_number, Type::N, default: 1
9
+ column 7..10, :detail_number, Type::N, default: 0
10
+ column 11..31, :bank_reference, Type::AN, default: ''
11
+ column 32..39, :transaction_code, Type::N, default: 0
12
+ column 40..40, :communication, Type::CommunicationType, default: ''
13
+ column 41..113, :communication, Type::Communication, default: ''
14
+ column 114..125, nil, Type::Blank
15
+ column 126..126, :next_code, Type::Flag, default: false
16
+ column 127..127, nil, Type::Blank, default: ''
17
+ column 128..128, :link_code, Type::Flag, default: false
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,17 @@
1
+ module Codabel
2
+ class Record
3
+ class Movement32 < Record
4
+ COMM_LENGTH = 1 + (115 - 11)
5
+
6
+ column 1..1, nil, Type::N, default: 3
7
+ column 2..2, nil, Type::N, default: 2
8
+ column 3..6, :sequence_number, Type::N, default: 1
9
+ column 7..10, :detail_number, Type::N, default: 0
10
+ column 11..115, :communication, Type::Communication, default: ''
11
+ column 116..125, nil, Type::Blank
12
+ column 126..126, :next_code, Type::Flag, default: false
13
+ column 127..127, nil, Type::Blank, default: ''
14
+ column 128..128, :link_code, Type::Flag, default: false
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ module Codabel
2
+ class Record
3
+ class Movement33 < Record
4
+ COMM_LENGTH = 1 + (100 - 11)
5
+
6
+ column 1..1, nil, Type::N, default: 3
7
+ column 2..2, nil, Type::N, default: 3
8
+ column 3..6, :sequence_number, Type::N, default: 1
9
+ column 7..10, :detail_number, Type::N, default: 0
10
+ column 11..100, :communication, Type::Communication, default: ''
11
+ column 101..125, nil, Type::Blank
12
+ column 126..126, :next_code, Type::Flag, default: false
13
+ column 127..127, nil, Type::Blank, default: ''
14
+ column 128..128, :link_code, Type::Flag, default: false
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,27 @@
1
+ module Codabel
2
+ class Record
3
+ class NewBalance < Record
4
+ FOLLOWING = { when_true: '1', when_false: '0' }.freeze
5
+
6
+ column 1..1, nil, Type::N, default: 8
7
+ column 2..4, :sequence_number_paper, Type::N, default: 0
8
+ column 5..41, :account, Type::AccountAndCurrency, default: ''
9
+ column 42..42, :balance, Type::AmountSign, default: 0
10
+ column 43..57, :balance, Type::Amount, default: 0
11
+ column 58..63, :balance_date, Type::Date, default: Date.today
12
+ column 64..127, nil, Type::AN, default: ''
13
+ column 128..128, :communication_follows, Type::Flag.new(**FOLLOWING), default: false
14
+
15
+ def balance
16
+ data[:balance] || 0
17
+ end
18
+
19
+ def validate!(file)
20
+ return unless (old_balance = file.find_record(OldBalance))
21
+
22
+ expected = old_balance.balance + file.find_records(Movement21).map(&:amount).sum
23
+ check!(expected == balance, "Invalid new balance: expected #{expected}, got #{balance}")
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,20 @@
1
+ module Codabel
2
+ class Record
3
+ class OldBalance < Record
4
+ column 1..1, nil, Type::N, default: 1
5
+ column 2..2, :account, Type::AccountStructure, default: 2
6
+ column 3..5, :sequence_number_paper, Type::N, default: 0
7
+ column 6..42, :account, Type::AccountAndCurrency, default: ''
8
+ column 43..43, :balance, Type::AmountSign, default: 0
9
+ column 44..58, :balance, Type::Amount, default: 0
10
+ column 59..64, :balance_date, Type::Date, default: Date.today
11
+ column 65..90, [:account, :holder_name], Type::AN, default: ''
12
+ column 91..125, :account, Type::AccountDescription, default: ''
13
+ column 126..128, :sequence_number, Type::N, default: 1
14
+
15
+ def balance
16
+ data[:balance] || 0
17
+ end
18
+ end
19
+ end
20
+ end