norma43_parser 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +4 -0
  3. data/LICENSE +22 -0
  4. data/README.md +85 -0
  5. data/Rakefile +1 -0
  6. data/doc/classes.jpg +0 -0
  7. data/doc/cuaderno_43_-_junio_2012.pdf +800 -1
  8. data/lib/norma43/line_handlers.rb +54 -0
  9. data/lib/norma43/line_parsers/file_format_validator.rb +19 -0
  10. data/lib/norma43/line_parsers/line_parser.rb +42 -0
  11. data/lib/norma43/line_parsers/line_parsers.rb +69 -0
  12. data/lib/norma43/line_processors.rb +60 -0
  13. data/lib/norma43/models.rb +85 -0
  14. data/lib/norma43/parser.rb +79 -0
  15. data/lib/norma43/utils/contexts.rb +54 -0
  16. data/lib/norma43/utils/string_helpers.rb +10 -0
  17. data/lib/norma43/utils/typecaster.rb +19 -0
  18. data/lib/norma43/version.rb +3 -0
  19. data/lib/norma43.rb +8 -0
  20. data/norma43_parser.gemspec +27 -0
  21. data/spec/example1_parse_spec.rb +91 -0
  22. data/spec/fixtures/example1.n43 +10 -0
  23. data/spec/norma43/line_parsers/account_end_spec.rb +51 -0
  24. data/spec/norma43/line_parsers/account_start_spec.rb +55 -0
  25. data/spec/norma43/line_parsers/additional_currency_spec.rb +27 -0
  26. data/spec/norma43/line_parsers/additional_items_spec.rb +23 -0
  27. data/spec/norma43/line_parsers/document_end_spec.rb +16 -0
  28. data/spec/norma43/line_parsers/document_start_spec.rb +32 -0
  29. data/spec/norma43/line_parsers/transaction_spec.rb +55 -0
  30. data/spec/norma43/line_processors/account_end_spec.rb +34 -0
  31. data/spec/norma43/line_processors/account_start_spec.rb +40 -0
  32. data/spec/norma43/line_processors/additional_currency_spec.rb +42 -0
  33. data/spec/norma43/line_processors/additional_items_spec.rb +42 -0
  34. data/spec/norma43/line_processors/document_end_spec.rb +30 -0
  35. data/spec/norma43/line_processors/document_start_spec.rb +27 -0
  36. data/spec/norma43/line_processors/transaction_spec.rb +42 -0
  37. data/spec/norma43/parser_spec.rb +23 -0
  38. data/spec/norma43_spec.rb +13 -0
  39. data/spec/spec_helper.rb +20 -0
  40. data/spec/support/shared_examples_for_values_line_parsers.rb +15 -0
  41. metadata +173 -0
@@ -0,0 +1,54 @@
1
+ require "norma43/line_parsers/line_parsers"
2
+ require "norma43/line_processors"
3
+
4
+ module Norma43
5
+ module LineHandlers
6
+ def self.mapping
7
+ {
8
+ "00" => self.document_start,
9
+ "88" => self.document_end,
10
+ "11" => self.account_start,
11
+ "33" => self.account_end,
12
+ "22" => self.transaction,
13
+ "23" => self.additional_item,
14
+ "24" => self.additional_currency
15
+ }
16
+ end
17
+
18
+ def self.document_start
19
+ Handler.new LineParsers::DocumentStart, LineProcessors::DocumentStart
20
+ end
21
+
22
+ def self.document_end
23
+ Handler.new LineParsers::DocumentEnd, LineProcessors::DocumentEnd
24
+ end
25
+
26
+ def self.account_start
27
+ Handler.new LineParsers::AccountStart, LineProcessors::AccountStart
28
+ end
29
+
30
+ def self.account_end
31
+ Handler.new LineParsers::AccountEnd, LineProcessors::AccountEnd
32
+ end
33
+
34
+ def self.transaction
35
+ Handler.new LineParsers::Transaction, LineProcessors::Transaction
36
+ end
37
+
38
+ def self.additional_item
39
+ Handler.new LineParsers::AdditionalItem, LineProcessors::AdditionalItem
40
+ end
41
+
42
+ def self.additional_currency
43
+ Handler.new LineParsers::AdditionalCurrency, LineProcessors::AdditionalCurrency
44
+ end
45
+
46
+ Handler = Struct.new :parser, :processor do
47
+ def process line, contexts
48
+ line_parser = self.parser.new(line)
49
+
50
+ processor.call line_parser, contexts
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,19 @@
1
+ require "norma43/line_parsers/line_parser"
2
+ module Norma43
3
+ class FileFormatValidator < LineParser
4
+ field :record_type, 0..1, :raw
5
+ field :file_type, 36..38
6
+
7
+ def has_document?; file_type=="00" end
8
+
9
+ def valid?
10
+ errors.empty?
11
+ end
12
+
13
+ def errors
14
+ errors = []
15
+ %w(11 00).include? record_type or errors << "Must start with 00 (was ”#{record_type}”)"
16
+ errors
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,42 @@
1
+ require "norma43/utils/typecaster"
2
+
3
+ module Norma43
4
+ class LineParser
5
+ attr_reader :line
6
+ def initialize line
7
+ @line = line
8
+ end
9
+
10
+ def attributes
11
+ self.class.field_names.each_with_object({}) do |field, attrs|
12
+ attrs[field] = self.public_send(field)
13
+ end
14
+ end
15
+
16
+ def self.field name, range, type = :string
17
+ self.field_names.push name
18
+
19
+ define_method name do
20
+ if range.is_a?(Array) # let multivalued attribute
21
+ range.map { |r| value_at_position(r, type) }.compact
22
+ else
23
+ value_at_position range, type
24
+ end
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def self.field_names
31
+ @field_names ||= []
32
+ end
33
+
34
+ def value_at_position range, type
35
+ typecast line[range].to_s.strip, type
36
+ end
37
+
38
+ def typecast value, type
39
+ Typecaster.cast value, type
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,69 @@
1
+ require "norma43/line_parsers/line_parser"
2
+
3
+ module Norma43
4
+ module LineParsers
5
+ class DocumentStart < LineParser
6
+ field :id, 2..13
7
+ field :created_at, 14..33, :time
8
+ field :delivery_number, 34..35, :integer
9
+ field :file_type, 36..38
10
+ field :name, 39..48
11
+ end
12
+
13
+ class DocumentEnd < LineParser
14
+ field :record_number, 20..25, :integer
15
+ end
16
+
17
+ class AccountStart < LineParser
18
+ field :bank_code, 2..5, :integer
19
+ field :branch_code, 6..9, :integer
20
+ field :account_number, 10..19, :integer
21
+ field :start_date, 20..25, :date
22
+ field :end_date, 26..31, :date
23
+ field :balance_code, 32, :integer
24
+ field :balance_amount, 33..46, :integer
25
+ field :currency_code, 47..49, :integer
26
+ field :information_mode_code, 50, :integer
27
+ field :abbreviated_name, 51..76
28
+ end
29
+
30
+ class AccountEnd < LineParser
31
+ field :bank_code, 2..5, :integer
32
+ field :branch_code, 6..9, :integer
33
+ field :account_number, 10..19, :integer
34
+ field :debit_entries, 20..24, :integer
35
+ field :debit_amount, 25..38, :integer
36
+ field :credit_entries, 39..43, :integer
37
+ field :credit_amount, 44..57, :integer
38
+ field :balance_code, 58, :integer
39
+ field :balance_amount, 59..72, :integer
40
+ field :currency_code, 73..75, :integer
41
+ end
42
+
43
+ class Transaction < LineParser
44
+ field :origin_branch_code, 6..9, :integer
45
+ field :transaction_date, 10..15, :date
46
+ field :value_date, 16..21, :date
47
+ field :shared_item, 22..23, :integer
48
+ field :own_item, 24..26, :integer
49
+ field :amount_code, 27, :integer
50
+ field :amount, 28..41, :integer
51
+ field :document_number, 42..51, :integer
52
+ field :reference_1, 52..63, :integer
53
+ field :reference_2, 64..79
54
+ end
55
+
56
+ class AdditionalItem < LineParser
57
+ field :data_code, 2..3, :integer
58
+ field :item_1, 4..41
59
+ field :item_2, 42..79
60
+ end
61
+
62
+ class AdditionalCurrency < LineParser
63
+ field :data_code, 2..3, :integer
64
+ field :currency_code, 4..6, :integer
65
+ field :amount, 7..20, :integer
66
+ field :free, 21..79
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,60 @@
1
+ require "norma43/models"
2
+ module Norma43
3
+ module LineProcessors
4
+ DocumentStart = ->(line, contexts) {
5
+ document = Models::Document.new line.attributes
6
+
7
+ contexts.add document
8
+
9
+ contexts
10
+ }
11
+
12
+ DocumentEnd = ->(line, contexts) {
13
+ contexts.move_to Models::Document
14
+ contexts.current.number_of_lines = line.record_number
15
+ contexts
16
+ }
17
+
18
+ AccountStart = ->(line, contexts) {
19
+ contexts.move_to Models::Document
20
+ account = Models::Account.new line.attributes
21
+ contexts.current.accounts << account
22
+ contexts.add account
23
+
24
+ contexts
25
+ }
26
+
27
+ AccountEnd = ->(line, contexts) {
28
+ contexts.move_to Models::Account
29
+ contexts.current.attributes = line.attributes
30
+ contexts
31
+ }
32
+
33
+ Transaction = ->(line, contexts) {
34
+ contexts.move_to Models::Account
35
+
36
+ transaction = Models::Transaction.new line.attributes
37
+
38
+ contexts.current.transactions << transaction
39
+
40
+ contexts.add transaction
41
+
42
+ contexts
43
+ }
44
+
45
+ AdditionalItem = ->(line, contexts) {
46
+ contexts.move_to Models::Transaction
47
+ additional_item = Models::AdditionalItem.new line.attributes
48
+ contexts.current.additional_items << additional_item
49
+ contexts
50
+ }
51
+
52
+ AdditionalCurrency = ->(line, contexts) {
53
+ contexts.move_to Models::Transaction
54
+ additional_currency = Models::AdditionalCurrency.new line.attributes
55
+ contexts.current.additional_currency = additional_currency
56
+ contexts
57
+ }
58
+
59
+ end
60
+ end
@@ -0,0 +1,85 @@
1
+ require "norma43/utils/string_helpers"
2
+ require 'virtus'
3
+
4
+ module Norma43
5
+ module Models
6
+ #forward declarations
7
+ class Account; end
8
+ class Transaction; end
9
+ class AdditionalItem; end
10
+ class AdditionalCurrency; end
11
+ DEBIT_CODE = 1
12
+ CREDIT_CODE = 2
13
+
14
+ class Document
15
+ include Virtus.model
16
+
17
+ attribute :id
18
+ attribute :created_at
19
+ attribute :delivery_number
20
+ attribute :file_type
21
+ attribute :name
22
+ attribute :number_of_lines
23
+ attribute :accounts, Array[Account]
24
+
25
+ def transaction_date
26
+ accounts.map(&:date).compact.first
27
+ end
28
+ end
29
+
30
+ class Account
31
+ include Virtus.model
32
+
33
+ attribute :bank_code
34
+ attribute :branch_code
35
+ attribute :account_number
36
+ attribute :start_date
37
+ attribute :end_date
38
+ attribute :balance_code
39
+ attribute :balance_amount
40
+ attribute :currency_code
41
+ attribute :information_mode_code
42
+ attribute :abbreviated_name
43
+ attribute :debit_entries
44
+ attribute :debit_amount
45
+ attribute :credit_entries
46
+ attribute :credit_amount
47
+ attribute :transactions, Array[Transaction]
48
+ end
49
+
50
+ class Transaction
51
+ include Virtus.model
52
+
53
+ attribute :origin_branch_code
54
+ attribute :transaction_date
55
+ attribute :value_date
56
+ attribute :shared_item
57
+ attribute :own_item
58
+ attribute :amount_code
59
+ attribute :amount
60
+ attribute :document_number
61
+ attribute :reference_1
62
+ attribute :reference_2
63
+ attribute :additional_items, Array[AdditionalItem]
64
+ attribute :additional_currency, AdditionalCurrency
65
+ def debit?; self.amount_code==DEBIT_CODE end
66
+ end
67
+
68
+ class AdditionalItem
69
+ include Virtus.model
70
+
71
+ attribute :data_code
72
+ attribute :item_1
73
+ attribute :item_2
74
+ end
75
+
76
+ class AdditionalCurrency
77
+ include Virtus.model
78
+
79
+ attribute :data_code
80
+ attribute :currency_code
81
+ attribute :amount
82
+ attribute :free
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,79 @@
1
+ require "norma43/line_parsers/file_format_validator"
2
+ require "norma43/line_handlers"
3
+ require "norma43/utils/contexts"
4
+
5
+ module Norma43
6
+ class InvalidFileFormatError < ArgumentError; end;
7
+
8
+ class Parser
9
+ attr_reader :file
10
+
11
+ # Parser.new accepts a File instance or a String
12
+ # A InvalidFileFormatError will be raised if file isn't in the Norma43 format
13
+ def initialize file
14
+ @file = file
15
+ validator = validate_file_format
16
+ @contexts = if validator.has_document?
17
+ Contexts.new
18
+ else
19
+ # in theory Norma43 says that files should start with DocumentStart but
20
+ # practically doesn't happen, so that we create one artificially
21
+ # to avoid corner cases in the processors
22
+ Contexts.new().tap { |ctx| ctx.add Models::Document.new }
23
+ end
24
+ end
25
+
26
+ def result
27
+ parse_lines(@contexts).result
28
+ end
29
+
30
+ protected
31
+
32
+ def lines
33
+ @lines ||= file.each_line
34
+ end
35
+
36
+ private
37
+
38
+ def validate_file_format
39
+ validator = FileFormatValidator.new first_line
40
+ raise InvalidFileFormatError.new(validator.errors.join(", ")) unless validator.valid?
41
+ validator
42
+ end
43
+
44
+ def parse_lines contexts
45
+ parse_lines parse_line(self.lines.next, contexts)
46
+
47
+ rescue StopIteration# because lines is an enumerator raises StopIteration on end
48
+ self.lines.rewind # Ensure we do not bomb out when calling result multiple times
49
+ contexts
50
+ end
51
+
52
+ # Look up a matching handler for the line and process it
53
+ # The process method on a handler always returns a Contexts object
54
+ def parse_line line, contexts
55
+ line = line.encode Encoding::UTF_8 if encode_lines?
56
+
57
+ handler = handler_for_line line
58
+
59
+ handler.process line, contexts
60
+ end
61
+
62
+ def handler_for_line line
63
+ LineHandlers.mapping.fetch line[0..1]
64
+ end
65
+
66
+ def encode_lines?
67
+ first_line.encoding != Encoding::UTF_8
68
+ end
69
+
70
+ def first_line
71
+ @first_line ||= begin
72
+ line = self.lines.peek
73
+ self.lines.rewind # peek seems to move the pointer when file is an actual File object
74
+
75
+ line
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,54 @@
1
+ module Norma43
2
+ class Contexts
3
+ def initialize containers = nil
4
+ Array(containers).compact.each do |container|
5
+ add container
6
+ end
7
+ end
8
+
9
+ def result
10
+ contexts.first
11
+ end
12
+
13
+ def current
14
+ contexts.last
15
+ end
16
+
17
+ def add container
18
+ contexts.push container
19
+ end
20
+
21
+ def move_up
22
+ contexts.pop
23
+ end
24
+
25
+ def move_to container_class
26
+ until current.is_a?(container_class) or current.nil?
27
+ move_up
28
+ end if contexts.any?
29
+ end
30
+
31
+ def move_to_or_add_to_parent container_class, parent_container_class
32
+ return self if current.is_a?(container_class)
33
+
34
+ until current.kind_of?(parent_container_class)
35
+ move_up
36
+ end
37
+
38
+ entity = container_class.new
39
+
40
+ setter_name = StringHelpers.underscore container_class.name.split("::").last
41
+ current.public_send "#{setter_name}=", entity
42
+
43
+ add entity
44
+
45
+ self
46
+ end
47
+
48
+ private
49
+
50
+ def contexts
51
+ @contexts ||= []
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,10 @@
1
+ module Norma43
2
+ module StringHelpers
3
+ def self.underscore word
4
+ word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
5
+ word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
6
+ word.tr!("-", "_")
7
+ word.downcase
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,19 @@
1
+ require "time"
2
+
3
+ module Norma43
4
+ module Typecaster
5
+ def self.cast value, type
6
+ casters.fetch(type).call(value) unless value == ""
7
+ end
8
+
9
+ def self.casters
10
+ {
11
+ integer: ->(value) { value.to_i },
12
+ time: ->(value) { Time.strptime(value, "%Y%m%d%H%M%S%N") },
13
+ date: ->(value) { Date.strptime(value, "%y%m%d") },
14
+ string: ->(value) { value unless value.match(/\A0+\Z/) },
15
+ raw: ->(value) { value }
16
+ }
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,3 @@
1
+ module Norma43
2
+ VERSION = "1.0.1"
3
+ end
data/lib/norma43.rb ADDED
@@ -0,0 +1,8 @@
1
+ require "norma43/version"
2
+ require "norma43/parser"
3
+
4
+ module Norma43
5
+ def self.parse text
6
+ Parser.new(text).result
7
+ end
8
+ end
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "norma43/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "norma43_parser"
8
+ spec.version = Norma43::VERSION
9
+ spec.authors = ["Sequra engineering"]
10
+ spec.email = ["dev@sequra.es"]
11
+ spec.summary = %q{Parses banks transactions files specified in rule 43}
12
+ spec.homepage = "https://github.com/sequra/norma43_parser"
13
+ spec.license = "MIT"
14
+
15
+ spec.required_ruby_version = "~> 2.0"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0")
18
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.5"
23
+ spec.add_development_dependency "rake"
24
+ spec.add_development_dependency "byebug"
25
+ spec.add_development_dependency "rspec"
26
+ spec.add_runtime_dependency "virtus"
27
+ end
@@ -0,0 +1,91 @@
1
+ require "norma43"
2
+
3
+ RSpec.describe Norma43 do
4
+ describe "parse" do
5
+
6
+ let(:document) do
7
+ file = File.open( File.join(__dir__, "fixtures/example1.n43"),
8
+ encoding: "iso-8859-1")
9
+ Norma43.parse file
10
+ end
11
+
12
+ it "finds one account" do
13
+ expect(document.accounts.size).to eq 1
14
+ end
15
+
16
+ describe "first account" do
17
+ let(:account) { document.accounts.first }
18
+
19
+ it "stores expected attributes from AccountStart parser" do
20
+ expect(account).to have_attributes({
21
+ "bank_code" => 9999,
22
+ "branch_code" => 1111,
23
+ "account_number" => 123456789,
24
+ "start_date" => Date.parse("2004-08-04"),
25
+ "end_date" => Date.parse("2004-09-05"),
26
+ "currency_code" => 1,
27
+ "information_mode_code" => 3,
28
+ "abbreviated_name" => "MY ACCOUNT"
29
+ })
30
+ end
31
+
32
+ it "stores expected attributes from AccountEnd parser" do
33
+ expect(account).to have_attributes({
34
+ "balance_code" => 2,
35
+ "balance_amount" => 78889999999999,
36
+ "debit_entries" => 4,
37
+ "debit_amount" => 4936,
38
+ "credit_entries" => 2,
39
+ "credit_amount" => 999999,
40
+ })
41
+ end
42
+
43
+ describe "transactions" do
44
+ it "finds all transactions" do
45
+ expect(account.transactions.size).to eq 4
46
+ end
47
+
48
+ describe "first transaction" do
49
+ let(:transaction) { account.transactions[0] }
50
+ it "stores expected attributes" do
51
+ expect(transaction).to have_attributes({
52
+ "origin_branch_code" => 6700,
53
+ "transaction_date" => Date.parse("2004-04-08"),
54
+ "value_date" => Date.parse("2004-04-08"),
55
+ "shared_item" => 4,
56
+ "own_item" => 7,
57
+ "amount_code" => 2,
58
+ "amount" => 1234,
59
+ "document_number" => 0,
60
+ "reference_1" => 0,
61
+ "reference_2" => nil,
62
+ })
63
+ end
64
+ end
65
+
66
+ it "each transaction has 5 additional_items as a maximum" do
67
+ account.transactions.each do |transaction|
68
+ expect(transaction.additional_items.size).to be <= 5
69
+ end
70
+ end
71
+
72
+ describe "first additional item" do
73
+ let(:additional_item) { account.transactions[0].additional_items[0]}
74
+ it "stores expected attributes" do
75
+ expect(additional_item).to have_attributes({
76
+ "data_code" => 1,
77
+ "item_1" => "XXXXXXXXX",
78
+ "item_2" => nil
79
+ })
80
+ end
81
+ end
82
+
83
+ describe "first additional currency" do
84
+ xit "stores expected attributes" do
85
+ # We don't have exaples to write test write when have
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,10 @@
1
+ 119999111101234567890408040409052000000001234569783MY ACCOUNT
2
+ 2212346700040408040408040072000000000012340000000000000000000000
3
+ 2301XXXXXXXXX
4
+ 2212346700040408040408040072000000000012340000000000000000000000
5
+ 2301A28152585
6
+ 2256781128040405040405040072000000000012340000000000000000000000
7
+ 2301XXXXXXXXX
8
+ 2256781127040805040805020092000000000012340000000000000000000000
9
+ 2301REF XXXXXXXXX
10
+ 3399991111012345678900004000000000049360000200000000999999278889999999999001
@@ -0,0 +1,51 @@
1
+ require "norma43/line_parsers/line_parsers"
2
+
3
+ module Norma43
4
+ module LineParsers
5
+ RSpec.describe AccountEnd do
6
+ let :account_end do
7
+ AccountEnd.new "3399991111012345678900004000000000049360000200000000999999278889999999999001 "
8
+ end
9
+
10
+ it "parses the bank code" do
11
+ expect(account_end.bank_code).to eq 9999
12
+ end
13
+
14
+ it "parses the branch code" do
15
+ expect(account_end.branch_code).to eq 1111
16
+ end
17
+
18
+ it "parses the account number" do
19
+ expect(account_end.account_number).to eq 123456789
20
+ end
21
+
22
+ it "parses the number of debit entries" do
23
+ expect(account_end.debit_entries).to eq 4
24
+ end
25
+
26
+ it "parses the total of debit amounts" do
27
+ expect(account_end.debit_amount).to eq 4936
28
+ end
29
+
30
+ it "parses the number of credit entries" do
31
+ expect(account_end.credit_entries).to eq 2
32
+ end
33
+
34
+ it "parses the total of credit amounts" do
35
+ expect(account_end.credit_amount).to eq 999999
36
+ end
37
+
38
+ it "parses the final balance code" do
39
+ expect(account_end.balance_code).to eq 2
40
+ end
41
+
42
+ it "parses the final balance amount" do
43
+ expect(account_end.balance_amount).to eq 78889999999999
44
+ end
45
+
46
+ it "parses the currency code" do
47
+ expect(account_end.currency_code).to eq 1
48
+ end
49
+ end
50
+ end
51
+ end