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
@@ -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: []