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
@@ -0,0 +1,73 @@
1
+ module Codabel
2
+ class Record
3
+ class Trailer < Record
4
+ FOLLOWING = { when_true: '1', when_false: '2' }.freeze
5
+
6
+ column 1..1, nil, Type::N, default: 9
7
+ column 2..16, nil, Type::AN, default: ''
8
+ column 17..22, :records_count, Type::N, default: 0
9
+ column 23..37, :debit, Type::Amount, default: 0
10
+ column 38..52, :credit, Type::Amount, default: 0
11
+ column 53..127, nil, Type::AN, default: ''
12
+ column 128..128, :file_follows, Type::Flag.new(**FOLLOWING), default: false
13
+
14
+ def auto_enrich(file)
15
+ enriched = data.merge(
16
+ records_count: data[:records_count] || count_records(file),
17
+ debit: data[:debit] || sum_debit(file),
18
+ credit: data[:credit] || sum_credit(file)
19
+ )
20
+ Trailer.new(enriched)
21
+ end
22
+
23
+ def validate!(file)
24
+ validate_records_count!(file)
25
+ validate_credit!(file)
26
+ validate_debit!(file)
27
+ end
28
+
29
+ private
30
+
31
+ def validate_records_count!(file)
32
+ return unless (count = data[:records_count])
33
+
34
+ expected = count_records(file)
35
+ check!(count == expected, "Invalid trailer record counts: expected #{expected}, got #{count}")
36
+ end
37
+
38
+ def validate_credit!(file)
39
+ return unless (credit = data[:credit])
40
+
41
+ expected = sum_credit(file)
42
+ check!(credit == expected, "Invalid trailer credit: expected #{expected}, got #{credit}")
43
+ end
44
+
45
+ def validate_debit!(file)
46
+ return unless (debit = data[:debit])
47
+
48
+ expected = sum_debit(file)
49
+ check!(debit == expected, "Invalid trailer debit: expected #{expected}, got #{debit}")
50
+ end
51
+
52
+ def count_records(file)
53
+ file.records.filter { |record| must_be_counted?(record) }.size
54
+ end
55
+
56
+ def must_be_counted?(record)
57
+ !(record.is_a?(Header) || record.is_a?(Trailer))
58
+ end
59
+
60
+ def sum_debit(file)
61
+ movements21(file).map(&:debit_amount).sum.abs
62
+ end
63
+
64
+ def sum_credit(file)
65
+ movements21(file).map(&:credit_amount).sum.abs
66
+ end
67
+
68
+ def movements21(file)
69
+ file.find_records(Movement21)
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,107 @@
1
+ module Codabel
2
+ class Record
3
+ class << self
4
+ def header(data = {})
5
+ Header.new(data)
6
+ end
7
+
8
+ def old_balance(data = {})
9
+ OldBalance.new(data)
10
+ end
11
+
12
+ def movement(data = {})
13
+ Movement.new(data)
14
+ end
15
+
16
+ def movement21(data = {})
17
+ Movement21.new(data)
18
+ end
19
+
20
+ def movement22(data = {})
21
+ Movement22.new(data)
22
+ end
23
+
24
+ def movement23(data = {})
25
+ Movement23.new(data)
26
+ end
27
+
28
+ def new_balance(data = {})
29
+ NewBalance.new(data)
30
+ end
31
+
32
+ def trailer(data = {})
33
+ Trailer.new(data)
34
+ end
35
+
36
+ def column(range, name, type, options = {})
37
+ last_index = columns.last&.range&.max || 0
38
+ raise Error, "Wrong column range #{range}, expected #{1+last_index}.." unless range.min == 1+last_index
39
+ raise Error, "Wrong column range #{range}" if range.max > 128
40
+
41
+ type = type.new if type.is_a?(Class)
42
+ add_column Column.new(range, name, type, options)
43
+ end
44
+
45
+ def add_column(column)
46
+ columns << column
47
+ end
48
+ private :add_column
49
+
50
+ def columns
51
+ @columns ||= []
52
+ end
53
+
54
+ def for(data)
55
+ new(data)
56
+ end
57
+
58
+ def required?(data, ignore: [])
59
+ columns
60
+ .reject { |column| ignore.any? { |path| column.path_starts_with?(path) } }
61
+ .any? { |column| column.specifics?(data) }
62
+ end
63
+ end
64
+
65
+ def initialize(data)
66
+ @data = data
67
+ end
68
+ attr_reader :data
69
+
70
+ def actual_records(_file)
71
+ [self]
72
+ end
73
+
74
+ def auto_enrich(_file)
75
+ [self]
76
+ end
77
+
78
+ def validate!(file)
79
+ end
80
+
81
+ def to_coda
82
+ str = self.class.columns.each_with_object('') do |column, memo|
83
+ memo << column.to_coda(self)
84
+ end
85
+ return str if str.length == 128
86
+
87
+ raise Error, "128 characters expected, got #{str.length}\n#{str}"
88
+ end
89
+
90
+ protected
91
+
92
+ def check!(assertion, message)
93
+ raise ValidationError, message unless assertion
94
+ end
95
+ end
96
+ end
97
+ require_relative 'record/header'
98
+ require_relative 'record/movement'
99
+ require_relative 'record/movement21'
100
+ require_relative 'record/movement22'
101
+ require_relative 'record/movement23'
102
+ require_relative 'record/movement31'
103
+ require_relative 'record/movement32'
104
+ require_relative 'record/movement33'
105
+ require_relative 'record/new_balance'
106
+ require_relative 'record/old_balance'
107
+ require_relative 'record/trailer'
@@ -0,0 +1,31 @@
1
+ module Codabel
2
+ class Type
3
+ class AccountAndCurrency < Type
4
+ def to_coda(value, length)
5
+ check!(length == 37, "Expected length to be 37, got #{length}")
6
+ account = Model::Account.dress(value)
7
+
8
+ case account.structure
9
+ when Model::Account::BELGIAN_BBAN
10
+ str = account.number.ljust(12, ' ') # 12 N Belgian account number
11
+ str << ' ' * 1 # 1 AN blank
12
+ str << account.currency.ljust(3, ' ') # 3 AN ISO currency code or blank
13
+ str << '0' # 1 N qualification code or blank
14
+ str << 'BE' # 2 AN ISO country code or blank
15
+ str << ' ' * 3 # 3 AN blank spaces
16
+ str << ' ' * 15 # 15 AN extension zone or blank
17
+ when Model::Account::FOREIGN_BBAN
18
+ str = account.number.ljust(34, ' ') # 34 AN foreign account number
19
+ str << account.currency.ljust(3, ' ') # 3 AN ISO currency code of the account
20
+ when Model::Account::BELGIAN_IBAN
21
+ str = account.number.ljust(31, ' ') # 31 AN IBAN (Belgian number)
22
+ str << ' ' * 3 # 3 AN extension zone or blank
23
+ str << account.currency.ljust(3, ' ') # 3 AN ISO currency code of the account
24
+ when Model::Account::FOREIGN_IBAN
25
+ str = account.number.ljust(34, ' ') # 34 AN foreign account number
26
+ str << account.currency.ljust(3, ' ') # 3 AN ISO currency code of the account
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,10 @@
1
+ module Codabel
2
+ class Type
3
+ class AccountDescription < Type
4
+ def to_coda(value, length)
5
+ description = Model::Account.dress(value).description
6
+ description.to_s.ljust(length, ' ')
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,19 @@
1
+ module Codabel
2
+ class Type
3
+ class AccountStructure < Type
4
+ CODES = {
5
+ Model::Account::BELGIAN_BBAN => '0',
6
+ Model::Account::FOREIGN_BBAN => '1',
7
+ Model::Account::BELGIAN_IBAN => '2',
8
+ Model::Account::FOREIGN_IBAN => '3'
9
+ }.freeze
10
+
11
+ def to_coda(value, length)
12
+ return value.to_s.rjust(length, '0') if %w[1 2 3 4].include?(value.to_s)
13
+
14
+ structure = Model::Account.dress(value).structure
15
+ CODES[structure].ljust(length, '0')
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,11 @@
1
+ module Codabel
2
+ class Type
3
+ class Amount < Type
4
+ def to_coda(value, length)
5
+ check!(value.is_a?(Integer), 'All amounts must be in cents')
6
+
7
+ (value.abs * 10).to_s.rjust(length, '0')
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ module Codabel
2
+ class Type
3
+ class AmountSign < Type
4
+ def to_coda(value, length)
5
+ (value >= 0.0 ? '0' : '1').rjust(length, '0')
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,12 @@
1
+ module Codabel
2
+ class Type
3
+ class AN < Type
4
+ def to_coda(value, length)
5
+ str = value.to_s
6
+ check!(str.length <= length, "Value `#{value}` is too long")
7
+
8
+ str.ljust(length, ' ')
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,9 @@
1
+ module Codabel
2
+ class Type
3
+ class Blank < Type
4
+ def to_coda(_value, length)
5
+ ' ' * length
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,37 @@
1
+ module Codabel
2
+ class Type
3
+ class Communication < Type
4
+ DEFAULT_STRUCTURED_CODE = '110'.freeze
5
+
6
+ def to_coda(value, length)
7
+ communication = communication_from(value)
8
+ check!(communication.length <= length, "Communication `#{communication}` is too long")
9
+
10
+ communication.ljust(length, ' ')
11
+ end
12
+
13
+ private
14
+
15
+ def communication_from(value)
16
+ case value
17
+ when Hash
18
+ extract_communication_from(value)
19
+ when String
20
+ value.to_s
21
+ else
22
+ check!(false, "Unexpected communication #{value}")
23
+ end
24
+ end
25
+
26
+ def extract_communication_from(hash)
27
+ if hash[:structured]
28
+ (hash[:structured_code] || DEFAULT_STRUCTURED_CODE) + hash[:structured].to_s
29
+ elsif hash[:unstructured]
30
+ hash[:unstructured].to_s
31
+ else
32
+ ''
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,43 @@
1
+ module Codabel
2
+ class Type
3
+ class CommunicationType < Type
4
+ def to_coda(value, length)
5
+ type = type_from(value)
6
+ code = to_code(type)
7
+ code.to_s.rjust(length, '0')
8
+ end
9
+
10
+ private
11
+
12
+ def type_from(communication)
13
+ case communication
14
+ when Hash
15
+ if communication[:structured]
16
+ :structured
17
+ else
18
+ :unstructured
19
+ end
20
+ when Symbol
21
+ communication
22
+ when String, NilClass
23
+ :unstructured
24
+ else
25
+ check!(false, "Unexpected communication #{communication.inspect}")
26
+ end
27
+ end
28
+
29
+ def to_code(type)
30
+ case type
31
+ when 0, 1
32
+ type
33
+ when :unstructured
34
+ 0
35
+ when :structured
36
+ 1
37
+ else
38
+ check!(false, "Unexpected communication type #{type.inspect}")
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,14 @@
1
+ module Codabel
2
+ class Type
3
+ class Date < Type
4
+ def to_coda(value, length)
5
+ check!(length == 6, 'Length of 6 expected')
6
+ return '0' * length if value.nil?
7
+
8
+ value = ::Date.parse(value) if value.is_a?(String)
9
+ value = value.to_date if value.respond_to?(:to_date)
10
+ value.strftime('%d%m%y')
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,10 @@
1
+ module Codabel
2
+ class Type
3
+ class Duplicate < Type
4
+ def to_coda(value, length)
5
+ str = value ? 'D' : ' '
6
+ str.rjust(length, ' ')
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,15 @@
1
+ module Codabel
2
+ class Type
3
+ class Flag < Type
4
+ def initialize(when_true: '1', when_false: '0')
5
+ @when_true = when_true
6
+ @when_false = when_false
7
+ end
8
+
9
+ def to_coda(value, length)
10
+ str = value ? @when_true : @when_false
11
+ str.rjust(length, ' ')
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module Codabel
2
+ class Type
3
+ class Holder < Type
4
+ def to_coda(value, length)
5
+ str = value.to_s
6
+ return str.ljust(length, ' ') if str.empty?
7
+
8
+ str = $1 if str =~ /^BE0(.*)/
9
+ check!(str.length <= length, "Value `#{value}` is too long")
10
+
11
+ str.rjust(length, '0')
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,12 @@
1
+ module Codabel
2
+ class Type
3
+ class N < Type
4
+ def to_coda(value, length)
5
+ str = value.to_i.to_s
6
+ check!(str.length <= length, "Value `#{value}` is too long")
7
+
8
+ str.rjust(length, '0')
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,29 @@
1
+ module Codabel
2
+ class Type
3
+ class ReturnTransactionType < Type
4
+ def to_coda(value, length)
5
+ rcode = case value
6
+ when '', NilClass
7
+ ''
8
+ when 1..5
9
+ value.to_s
10
+ when :reject
11
+ '1'
12
+ when :return
13
+ '2'
14
+ when :refund
15
+ '3'
16
+ when :reversal
17
+ '4'
18
+ when :cancellation
19
+ '5'
20
+ when /^[1-5]$/
21
+ value
22
+ else
23
+ check!(false, "Unexpected value `#{value}`")
24
+ end
25
+ rcode.ljust(length, ' ')
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,28 @@
1
+ module Codabel
2
+ class Type
3
+ def to_coda(_value, _length)
4
+ raise NotImplementedError, "#{self.class}#to_coda"
5
+ end
6
+
7
+ def check!(assertion, message)
8
+ return if assertion
9
+
10
+ raise TypeError, message + " for type #{self.class}"
11
+ end
12
+ end
13
+ end
14
+ require_relative 'type/account_structure'
15
+ require_relative 'type/account_and_currency'
16
+ require_relative 'type/account_description'
17
+ require_relative 'type/an'
18
+ require_relative 'type/amount'
19
+ require_relative 'type/amount_sign'
20
+ require_relative 'type/blank'
21
+ require_relative 'type/communication_type'
22
+ require_relative 'type/communication'
23
+ require_relative 'type/date'
24
+ require_relative 'type/duplicate'
25
+ require_relative 'type/flag'
26
+ require_relative 'type/n'
27
+ require_relative 'type/holder'
28
+ require_relative 'type/return_transaction_type'
@@ -0,0 +1,3 @@
1
+ module Codabel
2
+ VERSION = '1.0.0'.freeze
3
+ end
data/lib/codabel.rb ADDED
@@ -0,0 +1,11 @@
1
+ require 'date'
2
+ require 'ostruct'
3
+ module Codabel
4
+ end
5
+ require_relative 'codabel/version'
6
+ require_relative 'codabel/error'
7
+ require_relative 'codabel/model'
8
+ require_relative 'codabel/type'
9
+ require_relative 'codabel/column'
10
+ require_relative 'codabel/record'
11
+ require_relative 'codabel/file'
data/tasks/gem.rake ADDED
@@ -0,0 +1,16 @@
1
+ require 'rubygems/package_task'
2
+
3
+ gemspec_file = File.expand_path('../codabel.gemspec', __dir__)
4
+ gemspec = Kernel.eval(File.read(gemspec_file))
5
+ Gem::PackageTask.new(gemspec) do |t|
6
+ t.name = gemspec.name
7
+ t.version = gemspec.version
8
+ t.package_dir = 'pkg'
9
+ t.need_tar = false
10
+ t.need_tar_gz = false
11
+ t.need_tar_bz2 = false
12
+ t.need_zip = false
13
+ t.package_files = gemspec.files
14
+ t.tar_command = 'tar'
15
+ t.zip_command = 'zip'
16
+ end
data/tasks/test.rake ADDED
@@ -0,0 +1,17 @@
1
+ namespace :test do
2
+ require 'rspec/core/rake_task'
3
+
4
+ tests = []
5
+
6
+ desc 'Runs unit tests'
7
+ RSpec::Core::RakeTask.new(:unit) do |t|
8
+ t.pattern = 'spec/**/test_*.rb'
9
+ t.rspec_opts = ['-Ilib', '-Ispec', '--color', '--backtrace', '--format=progress']
10
+ end
11
+ tests << :unit
12
+
13
+ task all: tests
14
+ end
15
+
16
+ desc 'Runs all tests, unit then integration on examples'
17
+ task test: :'test:all'
metadata ADDED
@@ -0,0 +1,111 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: codabel
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Bernard Lambeau
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-11-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '13'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '13'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.6'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.6'
41
+ description: Codabel helps generating CODA files from structured information
42
+ email: bernard@flexio.app
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - Gemfile
48
+ - LICENSE.md
49
+ - README.md
50
+ - Rakefile
51
+ - lib/codabel.rb
52
+ - lib/codabel/column.rb
53
+ - lib/codabel/error.rb
54
+ - lib/codabel/file.rb
55
+ - lib/codabel/model.rb
56
+ - lib/codabel/model/account.rb
57
+ - lib/codabel/record.rb
58
+ - lib/codabel/record/header.rb
59
+ - lib/codabel/record/movement.rb
60
+ - lib/codabel/record/movement21.rb
61
+ - lib/codabel/record/movement22.rb
62
+ - lib/codabel/record/movement23.rb
63
+ - lib/codabel/record/movement31.rb
64
+ - lib/codabel/record/movement32.rb
65
+ - lib/codabel/record/movement33.rb
66
+ - lib/codabel/record/new_balance.rb
67
+ - lib/codabel/record/old_balance.rb
68
+ - lib/codabel/record/trailer.rb
69
+ - lib/codabel/type.rb
70
+ - lib/codabel/type/account_and_currency.rb
71
+ - lib/codabel/type/account_description.rb
72
+ - lib/codabel/type/account_structure.rb
73
+ - lib/codabel/type/amount.rb
74
+ - lib/codabel/type/amount_sign.rb
75
+ - lib/codabel/type/an.rb
76
+ - lib/codabel/type/blank.rb
77
+ - lib/codabel/type/communication.rb
78
+ - lib/codabel/type/communication_type.rb
79
+ - lib/codabel/type/date.rb
80
+ - lib/codabel/type/duplicate.rb
81
+ - lib/codabel/type/flag.rb
82
+ - lib/codabel/type/holder.rb
83
+ - lib/codabel/type/n.rb
84
+ - lib/codabel/type/return_transaction_type.rb
85
+ - lib/codabel/version.rb
86
+ - tasks/gem.rake
87
+ - tasks/test.rake
88
+ homepage: http://github.com/flexioapp/codabel
89
+ licenses:
90
+ - MIT
91
+ metadata: {}
92
+ post_install_message:
93
+ rdoc_options: []
94
+ require_paths:
95
+ - lib
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: '0'
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ requirements: []
107
+ rubygems_version: 3.1.4
108
+ signing_key:
109
+ specification_version: 4
110
+ summary: Codabel, a generator of CODA financial files.
111
+ test_files: []