codabel 1.0.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 +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