cfonb 0.0.0 → 0.0.2
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 +4 -4
- data/lib/cfonb/error.rb +15 -0
- data/lib/cfonb/line_parser/base.rb +82 -0
- data/lib/cfonb/line_parser/new_balance.rb +15 -0
- data/lib/cfonb/line_parser/operation.rb +24 -0
- data/lib/cfonb/line_parser/operation_detail.rb +18 -0
- data/lib/cfonb/line_parser/previous_balance.rb +15 -0
- data/lib/cfonb/line_parser.rb +21 -0
- data/lib/cfonb/operation.rb +54 -0
- data/lib/cfonb/operation_detail/fee.rb +19 -0
- data/lib/cfonb/operation_detail/lc2.rb +13 -0
- data/lib/cfonb/operation_detail/lcc.rb +13 -0
- data/lib/cfonb/operation_detail/lcs.rb +13 -0
- data/lib/cfonb/operation_detail/lib.rb +13 -0
- data/lib/cfonb/operation_detail/mmo.rb +22 -0
- data/lib/cfonb/operation_detail/nbe.rb +15 -0
- data/lib/cfonb/operation_detail/npy.rb +15 -0
- data/lib/cfonb/operation_detail/rcn.rb +20 -0
- data/lib/cfonb/operation_detail/ref.rb +18 -0
- data/lib/cfonb/operation_detail.rb +21 -0
- data/lib/cfonb/parser.rb +82 -0
- data/lib/cfonb/refinements/strings.rb +18 -0
- data/lib/cfonb/statement.rb +59 -0
- data/lib/cfonb.rb +33 -0
- data/spec/cfonb/line_parser/new_balance_spec.rb +26 -0
- data/spec/cfonb/line_parser/operation_detail_spec.rb +29 -0
- data/spec/cfonb/line_parser/operation_spec.rb +35 -0
- data/spec/cfonb/line_parser/previous_balance_spec.rb +26 -0
- data/spec/cfonb/operation_spec.rb +184 -0
- data/spec/cfonb/parser_spec.rb +469 -0
- data/spec/cfonb/statement_spec.rb +61 -0
- metadata +33 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1666b9858511859d77ab8b3c14a11ee214548b5da5154ee927cf7e51b14eca95
|
4
|
+
data.tar.gz: 292ed95e4250414dc82ef2967df62663cdadf529d72d8229d5609eee0fb3f0b1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9c74e319b7448e79163010bea1efeb5a5c5391bd0352b3c38d3145fd41f27ae60b0fe562d25b65698d207418288fb61dc4d49231be968f7dead6905bbc854132
|
7
|
+
data.tar.gz: a568a83590cfd3666d9ac96ba4dd6c09a39e73aa3a8136904cc24fe25df7d6b5b2f6d3fac74aee01e4ad57bf4455f9d4cf9e55ed993ca8f07f4b440eb103dc8d
|
data/lib/cfonb/error.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CFONB
|
4
|
+
class Error < StandardError; end
|
5
|
+
|
6
|
+
class ParserError < Error; end
|
7
|
+
|
8
|
+
class InvalidCodeError < ParserError; end
|
9
|
+
|
10
|
+
class UnstartedStatementError < ParserError; end
|
11
|
+
|
12
|
+
class UnstartedOperationError < ParserError; end
|
13
|
+
|
14
|
+
class UnfinishedStatementError < ParserError; end
|
15
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bigdecimal'
|
4
|
+
|
5
|
+
module CFONB
|
6
|
+
module LineParser
|
7
|
+
class Base
|
8
|
+
using CFONB::Refinements::Strings
|
9
|
+
|
10
|
+
BASE_DICTIONARY = [
|
11
|
+
['code', (0..1)].freeze,
|
12
|
+
['bank', (2..6)],
|
13
|
+
['branch', (11..15)],
|
14
|
+
['currency', (16..18)],
|
15
|
+
['scale', 19, proc { _1.to_i }],
|
16
|
+
['account', (21..31)],
|
17
|
+
['date', (34..39), proc { |value, instance| instance.send(:parse_date, value) }]
|
18
|
+
].freeze
|
19
|
+
|
20
|
+
AMOUNT_SPECIFIERS = {
|
21
|
+
'A' => { sign: 1, value: '1' },
|
22
|
+
'B' => { sign: 1, value: '2' },
|
23
|
+
'C' => { sign: 1, value: '3' },
|
24
|
+
'D' => { sign: 1, value: '4' },
|
25
|
+
'E' => { sign: 1, value: '5' },
|
26
|
+
'F' => { sign: 1, value: '6' },
|
27
|
+
'G' => { sign: 1, value: '7' },
|
28
|
+
'H' => { sign: 1, value: '8' },
|
29
|
+
'I' => { sign: 1, value: '9' },
|
30
|
+
'{' => { sign: 1, value: '0' },
|
31
|
+
'J' => { sign: -1, value: '1' },
|
32
|
+
'K' => { sign: -1, value: '2' },
|
33
|
+
'L' => { sign: -1, value: '3' },
|
34
|
+
'M' => { sign: -1, value: '4' },
|
35
|
+
'N' => { sign: -1, value: '5' },
|
36
|
+
'O' => { sign: -1, value: '6' },
|
37
|
+
'P' => { sign: -1, value: '7' },
|
38
|
+
'Q' => { sign: -1, value: '8' },
|
39
|
+
'R' => { sign: -1, value: '9' },
|
40
|
+
'}' => { sign: -1, value: '0' }
|
41
|
+
}.transform_values(&:freeze).freeze
|
42
|
+
|
43
|
+
attr_reader :body, *BASE_DICTIONARY.map(&:first)
|
44
|
+
|
45
|
+
def initialize(input)
|
46
|
+
@body = input
|
47
|
+
(BASE_DICTIONARY + self.class::DICTIONARY).each { parse_attribute(*_1) }
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def parse_attribute(attr, position, method = nil)
|
53
|
+
input = body[position]&.strip
|
54
|
+
value = method ? method.call(input, self) : input
|
55
|
+
|
56
|
+
instance_variable_set(:"@#{attr}", value)
|
57
|
+
end
|
58
|
+
|
59
|
+
def parse_amount(input)
|
60
|
+
specifier = AMOUNT_SPECIFIERS[input.last]
|
61
|
+
raise ParserError, "Invalid specifier '#{input.last}' for line #{input}" unless specifier
|
62
|
+
|
63
|
+
specifier[:sign] * BigDecimal(input[0..-2] + specifier[:value]) / (10**scale)
|
64
|
+
end
|
65
|
+
|
66
|
+
def parse_date(input)
|
67
|
+
# This won't work after 2060
|
68
|
+
return if input.strip.empty?
|
69
|
+
|
70
|
+
input_with_year = if input.last(2).to_i > 60
|
71
|
+
"#{input[0..3]}19#{input[4..5]}"
|
72
|
+
else
|
73
|
+
"#{input[0..3]}20#{input[4..5]}"
|
74
|
+
end
|
75
|
+
|
76
|
+
Date.strptime(input_with_year, '%d%m%Y')
|
77
|
+
rescue Date::Error
|
78
|
+
raise ParserError, "Invalid date '#{input}' for line #{input}"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CFONB
|
4
|
+
module LineParser
|
5
|
+
class NewBalance < Base
|
6
|
+
DICTIONARY = [
|
7
|
+
['amount', (90..103), proc { |value, instance| instance.send(:parse_amount, value) }]
|
8
|
+
].freeze
|
9
|
+
|
10
|
+
attr_reader(*DICTIONARY.map(&:first))
|
11
|
+
|
12
|
+
CFONB::LineParser.register(CFONB::Parser::NEW_BALANCE_CODE, self)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CFONB
|
4
|
+
module LineParser
|
5
|
+
class Operation < Base
|
6
|
+
DICTIONARY = [
|
7
|
+
['internal_operation_code', (7..10)],
|
8
|
+
['interbank_operation_code', (32..33)],
|
9
|
+
['rejection_code', (40..41)],
|
10
|
+
['value_date', (42..47), proc { |value, instance| instance.send(:parse_date, value) }],
|
11
|
+
['label', (48..79)],
|
12
|
+
['number', (81..87), proc { _1.to_i }],
|
13
|
+
['exoneration_code', 88],
|
14
|
+
['unavailability_code', 89],
|
15
|
+
['reference', (104..119)],
|
16
|
+
['amount', (90..103), proc { |value, instance| instance.send(:parse_amount, value) }]
|
17
|
+
].freeze
|
18
|
+
|
19
|
+
attr_reader(*DICTIONARY.map(&:first))
|
20
|
+
|
21
|
+
CFONB::LineParser.register(CFONB::Parser::OPERATION_CODE, self)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CFONB
|
4
|
+
module LineParser
|
5
|
+
class OperationDetail < Base
|
6
|
+
DICTIONARY = [
|
7
|
+
['internal_operation_code', (7..10)],
|
8
|
+
['interbank_operation_code', (32..33)],
|
9
|
+
['detail_code', (45..47)],
|
10
|
+
['detail', (48..117)]
|
11
|
+
].freeze
|
12
|
+
|
13
|
+
attr_reader(*DICTIONARY.map(&:first))
|
14
|
+
|
15
|
+
CFONB::LineParser.register(CFONB::Parser::OPERATION_DETAIL_CODE, self)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CFONB
|
4
|
+
module LineParser
|
5
|
+
class PreviousBalance < Base
|
6
|
+
DICTIONARY = [
|
7
|
+
['amount', (90..103), proc { |value, instance| instance.send(:parse_amount, value) }]
|
8
|
+
].freeze
|
9
|
+
|
10
|
+
attr_reader(*DICTIONARY.map(&:first))
|
11
|
+
|
12
|
+
CFONB::LineParser.register(CFONB::Parser::PREVIOUS_BALANCE_CODE, self)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CFONB
|
4
|
+
module LineParser
|
5
|
+
using CFONB::Refinements::Strings
|
6
|
+
|
7
|
+
@parsers = {}
|
8
|
+
|
9
|
+
def self.register(code, klass)
|
10
|
+
@parsers[code] = klass
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.for(code)
|
14
|
+
@parsers[code]
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.parse(input)
|
18
|
+
self.for(input.first(2)).new(input)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CFONB
|
4
|
+
class Operation
|
5
|
+
BASE_ATTRIBUTES = %i[
|
6
|
+
raw
|
7
|
+
amount
|
8
|
+
currency
|
9
|
+
date
|
10
|
+
exoneration_code
|
11
|
+
interbank_code
|
12
|
+
internal_code
|
13
|
+
label
|
14
|
+
number
|
15
|
+
reference
|
16
|
+
rejection_code
|
17
|
+
unavailability_code
|
18
|
+
value_date
|
19
|
+
].freeze
|
20
|
+
|
21
|
+
attr_accessor(*BASE_ATTRIBUTES)
|
22
|
+
|
23
|
+
def initialize(line)
|
24
|
+
self.raw = line.body
|
25
|
+
self.internal_code = line.internal_operation_code
|
26
|
+
self.interbank_code = line.interbank_operation_code
|
27
|
+
self.rejection_code = line.rejection_code
|
28
|
+
self.exoneration_code = line.exoneration_code
|
29
|
+
self.unavailability_code = line.unavailability_code
|
30
|
+
self.currency = line.currency
|
31
|
+
self.amount = line.amount
|
32
|
+
self.date = line.date
|
33
|
+
self.value_date = line.value_date
|
34
|
+
self.label = line.label.strip
|
35
|
+
self.number = line.number
|
36
|
+
self.reference = line.reference
|
37
|
+
end
|
38
|
+
|
39
|
+
def merge_detail(line)
|
40
|
+
self.raw += "\n#{line.body}"
|
41
|
+
OperationDetail.for(line)&.apply(self, line)
|
42
|
+
end
|
43
|
+
|
44
|
+
def type_code
|
45
|
+
"#{interbank_code}#{direction}"
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def direction
|
51
|
+
amount.positive? ? 'C' : 'D'
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CFONB
|
4
|
+
module OperationDetail
|
5
|
+
class FEE
|
6
|
+
ATTRIBUTES = %i[fee fee_currency].freeze
|
7
|
+
|
8
|
+
def self.apply(operation, line)
|
9
|
+
operation.fee_currency = line.detail[0..2]
|
10
|
+
scale = line.detail[3].to_i
|
11
|
+
sign = operation.amount <=> 0 # the detail amount is unsigned
|
12
|
+
|
13
|
+
operation.fee = sign * BigDecimal(line.detail[4..17]) / (10**scale)
|
14
|
+
end
|
15
|
+
|
16
|
+
CFONB::OperationDetail.register('FEE', self)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bigdecimal'
|
4
|
+
|
5
|
+
module CFONB
|
6
|
+
module OperationDetail
|
7
|
+
class MMO
|
8
|
+
ATTRIBUTES = %i[original_currency original_amount].freeze
|
9
|
+
|
10
|
+
def self.apply(operation, line)
|
11
|
+
operation.original_currency = line.detail[0..2]
|
12
|
+
|
13
|
+
scale = line.detail[3].to_i
|
14
|
+
sign = operation.amount <=> 0 # the detail amount is unsigned
|
15
|
+
|
16
|
+
operation.original_amount = sign * BigDecimal(line.detail[4..17]) / (10**scale)
|
17
|
+
end
|
18
|
+
|
19
|
+
CFONB::OperationDetail.register('MMO', self)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CFONB
|
4
|
+
module OperationDetail
|
5
|
+
class NBE
|
6
|
+
ATTRIBUTES = %i[creditor].freeze
|
7
|
+
|
8
|
+
def self.apply(operation, line)
|
9
|
+
operation.creditor = line.detail.strip
|
10
|
+
end
|
11
|
+
|
12
|
+
CFONB::OperationDetail.register('NBE', self)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CFONB
|
4
|
+
module OperationDetail
|
5
|
+
class NPY
|
6
|
+
ATTRIBUTES = %i[debtor].freeze
|
7
|
+
|
8
|
+
def self.apply(operation, line)
|
9
|
+
operation.debtor = line.detail.strip
|
10
|
+
end
|
11
|
+
|
12
|
+
CFONB::OperationDetail.register('NPY', self)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CFONB
|
4
|
+
module OperationDetail
|
5
|
+
class RCN
|
6
|
+
ATTRIBUTES = %i[reference purpose].freeze
|
7
|
+
|
8
|
+
def self.apply(operation, line)
|
9
|
+
operation.reference = [
|
10
|
+
operation.reference,
|
11
|
+
line.detail[0..34].strip
|
12
|
+
].compact.join(' - ')
|
13
|
+
|
14
|
+
operation.purpose = line.detail[35..-1]&.strip
|
15
|
+
end
|
16
|
+
|
17
|
+
CFONB::OperationDetail.register('RCN', self)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CFONB
|
4
|
+
module OperationDetail
|
5
|
+
class REF
|
6
|
+
ATTRIBUTES = %i[reference].freeze
|
7
|
+
|
8
|
+
def self.apply(operation, line)
|
9
|
+
operation.reference = [
|
10
|
+
operation.reference,
|
11
|
+
line.detail.strip
|
12
|
+
].compact.join(' - ')
|
13
|
+
end
|
14
|
+
|
15
|
+
CFONB::OperationDetail.register('REF', self)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CFONB
|
4
|
+
module OperationDetail
|
5
|
+
@details = {}
|
6
|
+
|
7
|
+
def self.register(code, klass)
|
8
|
+
if klass.const_defined?(:ATTRIBUTES)
|
9
|
+
Operation.class_eval do
|
10
|
+
attr_accessor(*klass::ATTRIBUTES)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
@details[code] = klass
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.for(line)
|
18
|
+
@details[line.detail_code]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/cfonb/parser.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CFONB
|
4
|
+
class Parser
|
5
|
+
using CFONB::Refinements::Strings
|
6
|
+
|
7
|
+
CODES = [
|
8
|
+
PREVIOUS_BALANCE_CODE = '01',
|
9
|
+
OPERATION_CODE = '04',
|
10
|
+
OPERATION_DETAIL_CODE = '05',
|
11
|
+
NEW_BALANCE_CODE = '07'
|
12
|
+
].freeze
|
13
|
+
|
14
|
+
def initialize(input)
|
15
|
+
@input = input
|
16
|
+
end
|
17
|
+
|
18
|
+
def parse(optimistic: false)
|
19
|
+
@statements = []
|
20
|
+
@current_statement = nil
|
21
|
+
@current_operation = nil
|
22
|
+
@optimistic = optimistic
|
23
|
+
|
24
|
+
each_line { parse_line(_1) }
|
25
|
+
|
26
|
+
statements
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
attr_reader :input, :statements, :current_statement, :current_operation, :optimistic
|
32
|
+
|
33
|
+
def each_line
|
34
|
+
input.each_line do |line|
|
35
|
+
(line.size / 120).times do |index|
|
36
|
+
start = index * 120
|
37
|
+
finish = start + 119
|
38
|
+
|
39
|
+
yield line[start..finish]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def parse_line(line)
|
45
|
+
return if line.strip.empty?
|
46
|
+
raise InvalidCodeError, "Invalid line code '#{line.first(2)}'" unless CODES.include?(line.first(2))
|
47
|
+
|
48
|
+
line = CFONB::LineParser.parse(line)
|
49
|
+
|
50
|
+
case line.code
|
51
|
+
when PREVIOUS_BALANCE_CODE
|
52
|
+
return handle_error(UnfinishedStatementError) if current_statement
|
53
|
+
|
54
|
+
@current_statement = CFONB::Statement.new(line)
|
55
|
+
when OPERATION_CODE
|
56
|
+
return handle_error(UnstartedStatementError) unless current_statement
|
57
|
+
|
58
|
+
current_statement.operations << current_operation if current_operation
|
59
|
+
@current_operation = CFONB::Operation.new(line)
|
60
|
+
when OPERATION_DETAIL_CODE
|
61
|
+
return handle_error(UnstartedOperationError) unless current_operation
|
62
|
+
|
63
|
+
current_operation.merge_detail(line)
|
64
|
+
when NEW_BALANCE_CODE
|
65
|
+
return handle_error(UnstartedStatementError) unless current_statement
|
66
|
+
|
67
|
+
current_statement.operations << current_operation if current_operation
|
68
|
+
current_statement.merge_new_balance(line)
|
69
|
+
statements << current_statement
|
70
|
+
|
71
|
+
@current_statement = nil
|
72
|
+
@current_operation = nil
|
73
|
+
end
|
74
|
+
rescue CFONB::ParserError => e
|
75
|
+
handle_error(e)
|
76
|
+
end
|
77
|
+
|
78
|
+
def handle_error(error)
|
79
|
+
raise error unless optimistic
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CFONB
|
4
|
+
class Statement
|
5
|
+
attr_accessor(
|
6
|
+
*%i[
|
7
|
+
begin_raw end_raw
|
8
|
+
bank branch currency account
|
9
|
+
from from_balance
|
10
|
+
to to_balance
|
11
|
+
operations
|
12
|
+
]
|
13
|
+
)
|
14
|
+
|
15
|
+
def initialize(line)
|
16
|
+
self.begin_raw = line.body
|
17
|
+
self.bank = line.bank
|
18
|
+
self.branch = line.branch
|
19
|
+
self.currency = line.currency
|
20
|
+
self.account = line.account
|
21
|
+
self.from = line.date
|
22
|
+
self.from_balance = line.amount
|
23
|
+
self.operations = []
|
24
|
+
end
|
25
|
+
|
26
|
+
def merge_new_balance(line)
|
27
|
+
self.end_raw = line.body
|
28
|
+
self.to = line.date
|
29
|
+
self.to_balance = line.amount
|
30
|
+
end
|
31
|
+
|
32
|
+
def raw
|
33
|
+
[
|
34
|
+
begin_raw,
|
35
|
+
operations.map(&:raw),
|
36
|
+
end_raw
|
37
|
+
].join("\n")
|
38
|
+
end
|
39
|
+
|
40
|
+
def rib
|
41
|
+
# https://fr.wikipedia.org/wiki/Clé_RIB
|
42
|
+
key = 97 - ((
|
43
|
+
(bank.to_i * 89) +
|
44
|
+
(branch.to_i * 15) +
|
45
|
+
(account.upcase.tr('A-IJ-RS-Z', '1-91-92-9').to_i * 3)
|
46
|
+
) % 97)
|
47
|
+
|
48
|
+
"#{bank}#{branch}#{account}#{key.to_s.rjust(2, '0')}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def iban
|
52
|
+
# https://fr.wikipedia.org/wiki/International_Bank_Account_Number
|
53
|
+
normalized_rib = "#{rib}FR00".upcase.gsub(/[A-Z]/) { _1.ord - 55 }
|
54
|
+
key = 98 - (normalized_rib.to_i % 97)
|
55
|
+
|
56
|
+
"FR#{key.to_s.rjust(2, '0')}#{rib}"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/cfonb.rb
CHANGED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
require_relative 'cfonb/refinements/strings'
|
4
|
+
|
5
|
+
require_relative 'cfonb/error'
|
6
|
+
require_relative 'cfonb/parser'
|
7
|
+
require_relative 'cfonb/statement'
|
8
|
+
require_relative 'cfonb/operation'
|
9
|
+
require_relative 'cfonb/operation_detail'
|
10
|
+
|
11
|
+
require_relative 'cfonb/line_parser'
|
12
|
+
require_relative 'cfonb/line_parser/base'
|
13
|
+
require_relative 'cfonb/line_parser/previous_balance'
|
14
|
+
require_relative 'cfonb/line_parser/operation'
|
15
|
+
require_relative 'cfonb/line_parser/operation_detail'
|
16
|
+
require_relative 'cfonb/line_parser/new_balance'
|
17
|
+
|
18
|
+
require_relative 'cfonb/operation_detail/lib'
|
19
|
+
require_relative 'cfonb/operation_detail/lcc'
|
20
|
+
require_relative 'cfonb/operation_detail/lc2'
|
21
|
+
require_relative 'cfonb/operation_detail/lcs'
|
22
|
+
require_relative 'cfonb/operation_detail/mmo'
|
23
|
+
require_relative 'cfonb/operation_detail/nbe'
|
24
|
+
require_relative 'cfonb/operation_detail/npy'
|
25
|
+
require_relative 'cfonb/operation_detail/rcn'
|
26
|
+
require_relative 'cfonb/operation_detail/ref'
|
27
|
+
require_relative 'cfonb/operation_detail/fee'
|
28
|
+
|
29
|
+
module CFONB
|
30
|
+
def self.parse(input, optimistic: false)
|
31
|
+
Parser.new(input).parse(optimistic: optimistic)
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cfonb'
|
4
|
+
|
5
|
+
describe CFONB::LineParser::NewBalance do
|
6
|
+
describe '.initialize' do
|
7
|
+
subject(:line) { described_class.new(input) }
|
8
|
+
|
9
|
+
let(:input) do
|
10
|
+
'0115589 01020EUR2 98765432100 150519 0000000001904L150519160519 '
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'correctly parses a line' do
|
14
|
+
expect(line).to have_attributes(
|
15
|
+
'code' => '01',
|
16
|
+
'bank' => '15589',
|
17
|
+
'branch' => '01020',
|
18
|
+
'currency' => 'EUR',
|
19
|
+
'scale' => 2,
|
20
|
+
'account' => '98765432100',
|
21
|
+
'date' => Date.new(2019, 5, 15),
|
22
|
+
'amount' => -190.43
|
23
|
+
)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|