norma43_parser 2.1.1 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +22 -0
  3. data/.github/workflows/release.yml +33 -0
  4. data/.rspec +1 -0
  5. data/CHANGELOG.md +26 -10
  6. data/Gemfile +2 -2
  7. data/Gemfile.lock +51 -51
  8. data/README.md +2 -0
  9. data/lib/norma43/models/account/spanish_iban.rb +107 -0
  10. data/lib/norma43/models/account.rb +63 -0
  11. data/lib/norma43/models/additional_currency.rb +22 -0
  12. data/lib/norma43/models/additional_item.rb +20 -0
  13. data/lib/norma43/models/document.rb +42 -0
  14. data/lib/norma43/models/mixins/attributes_assignment.rb +33 -0
  15. data/lib/norma43/models/transaction.rb +51 -0
  16. data/lib/norma43/models.rb +0 -78
  17. data/lib/norma43/version.rb +1 -1
  18. data/norma43_parser.gemspec +3 -6
  19. data/spec/example1_parse_spec.rb +1 -1
  20. data/spec/norma43/line_processors/account_end_spec.rb +1 -4
  21. data/spec/norma43/line_processors/account_start_spec.rb +3 -3
  22. data/spec/norma43/line_processors/additional_currency_spec.rb +3 -3
  23. data/spec/norma43/line_processors/{additional_items_spec.rb → additional_item_spec.rb} +3 -3
  24. data/spec/norma43/line_processors/document_end_spec.rb +3 -6
  25. data/spec/norma43/line_processors/document_start_spec.rb +3 -3
  26. data/spec/norma43/line_processors/transaction_spec.rb +3 -3
  27. data/spec/norma43/models/account/spanish_iban_spec.rb +107 -0
  28. data/spec/norma43/models/account_spec.rb +54 -0
  29. data/spec/norma43/models/document_spec.rb +35 -0
  30. data/spec/norma43/models/transaction_spec.rb +25 -0
  31. data/spec/norma43_spec.rb +1 -1
  32. data/spec/spec_helper.rb +3 -0
  33. data/spec/support/shared_examples_for_models.rb +61 -0
  34. metadata +29 -37
@@ -1,86 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "virtus"
4
-
5
3
  module Norma43
6
4
  module Models
7
- # forward declarations
8
- class Account; end
9
- class Transaction; end
10
- class AdditionalItem; end
11
- class AdditionalCurrency; end
12
5
  DEBIT_CODE = 1
13
6
  CREDIT_CODE = 2
14
-
15
- class Document
16
- include Virtus.model
17
-
18
- attribute :id
19
- attribute :created_at
20
- attribute :delivery_number
21
- attribute :file_type
22
- attribute :name
23
- attribute :number_of_lines
24
- attribute :accounts, Array[Account]
25
-
26
- def transaction_date
27
- accounts.map(&:date).compact.first
28
- end
29
- end
30
-
31
- class Account
32
- include Virtus.model
33
-
34
- attribute :bank_code
35
- attribute :branch_code
36
- attribute :account_number
37
- attribute :start_date
38
- attribute :end_date
39
- attribute :balance_code
40
- attribute :balance_amount
41
- attribute :currency_code
42
- attribute :information_mode_code
43
- attribute :abbreviated_name
44
- attribute :debit_entries
45
- attribute :debit_amount
46
- attribute :credit_entries
47
- attribute :credit_amount
48
- attribute :transactions, Array[Transaction]
49
- end
50
-
51
- class Transaction
52
- include Virtus.model
53
-
54
- attribute :origin_branch_code
55
- attribute :transaction_date
56
- attribute :value_date
57
- attribute :shared_item
58
- attribute :own_item
59
- attribute :amount_code
60
- attribute :amount
61
- attribute :document_number
62
- attribute :reference_1
63
- attribute :reference_2
64
- attribute :additional_items, Array[AdditionalItem]
65
- attribute :additional_currency, AdditionalCurrency
66
- def debit?; self.amount_code == DEBIT_CODE end
67
- end
68
-
69
- class AdditionalItem
70
- include Virtus.model
71
-
72
- attribute :data_code
73
- attribute :item_1
74
- attribute :item_2
75
- end
76
-
77
- class AdditionalCurrency
78
- include Virtus.model
79
-
80
- attribute :data_code
81
- attribute :currency_code
82
- attribute :amount
83
- attribute :free
84
- end
85
7
  end
86
8
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Norma43
4
- VERSION = "2.1.1"
4
+ VERSION = "3.1.0"
5
5
  end
@@ -13,17 +13,14 @@ Gem::Specification.new do |spec|
13
13
  spec.homepage = "https://github.com/sequra/norma43_parser"
14
14
  spec.license = "MIT"
15
15
 
16
- spec.required_ruby_version = "~> 2.4"
16
+ spec.required_ruby_version = ">= 3.0"
17
17
 
18
18
  spec.files = `git ls-files -z`.split("\x0")
19
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
19
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
21
20
  spec.require_paths = ["lib"]
22
21
 
23
- spec.add_runtime_dependency "virtus", "~> 1.0"
24
22
  spec.add_runtime_dependency "zeitwerk", "~> 2.0"
25
23
 
26
- spec.add_development_dependency "bundler", "~> 1.0"
27
- spec.add_development_dependency "rake", "~> 13.0"
28
- spec.add_development_dependency "rspec", "~> 3.0"
24
+ spec.add_development_dependency "rake", "~> 13.0"
25
+ spec.add_development_dependency "rspec", "~> 3.9"
29
26
  end
@@ -26,7 +26,7 @@ RSpec.describe Norma43 do
26
26
  "end_date" => Date.parse("2004-09-05"),
27
27
  "currency_code" => 1,
28
28
  "information_mode_code" => 3,
29
- "abbreviated_name" => "MY ACCOUNT"
29
+ "abbreviated_name" => "MY ACCOUNT",
30
30
  )
31
31
  end
32
32
 
@@ -3,10 +3,7 @@
3
3
  module Norma43
4
4
  module LineProcessors
5
5
  RSpec.describe AccountEnd do
6
- let :line do
7
- double "Line", attributes: {}
8
- end
9
-
6
+ let(:line) { instance_double(LineParsers::AccountEnd, attributes: {}) }
10
7
  let(:account) { Norma43::Models::Account.new }
11
8
  let(:contexts) { Norma43::Utils::Contexts.new(
12
9
  [
@@ -2,8 +2,8 @@
2
2
 
3
3
  module Norma43
4
4
  module LineProcessors
5
- RSpec.describe "AccountStart" do
6
- let(:line) { double "Line", attributes: {} }
5
+ RSpec.describe AccountStart do
6
+ let(:line) { instance_double(LineParsers::AccountStart, attributes: {}) }
7
7
  let(:document) { Norma43::Models::Document.new }
8
8
  let(:contexts) { Norma43::Utils::Contexts.new }
9
9
 
@@ -20,7 +20,7 @@ module Norma43
20
20
  end
21
21
 
22
22
  context "when AccountStart is called" do
23
- let(:fake_account) { double "Norma43::Models::Account" }
23
+ let(:fake_account) { instance_double(Norma43::Models::Account) }
24
24
  before do
25
25
  allow(Norma43::Models::Account).to receive(:new) { fake_account }
26
26
  end
@@ -2,8 +2,8 @@
2
2
 
3
3
  module Norma43
4
4
  module LineProcessors
5
- RSpec.describe "AdditionalCurrency" do
6
- let(:line) { double "Line", attributes: {} }
5
+ RSpec.describe AdditionalCurrency do
6
+ let(:line) { instance_double(LineParsers::AdditionalCurrency, attributes: {}) }
7
7
  let(:transaction) { Norma43::Models::Transaction.new }
8
8
  let(:contexts) { Norma43::Utils::Contexts.new(
9
9
  [
@@ -22,7 +22,7 @@ module Norma43
22
22
  end
23
23
 
24
24
  context "when AdditionalCurrency is called" do
25
- let(:fake_additional_currency) { double "Models::AdditionalCurrency" }
25
+ let(:fake_additional_currency) { instance_double(Models::AdditionalCurrency) }
26
26
  before do
27
27
  allow(Models::AdditionalCurrency).to receive(:new) { fake_additional_currency }
28
28
  end
@@ -2,8 +2,8 @@
2
2
 
3
3
  module Norma43
4
4
  module LineProcessors
5
- RSpec.describe "AdditionalItems" do
6
- let(:line) { double "Line", attributes: {} }
5
+ RSpec.describe AdditionalItem do
6
+ let(:line) { instance_double(LineParsers::AdditionalItem, attributes: {}) }
7
7
  let(:transaction) { Norma43::Models::Transaction.new }
8
8
  let(:contexts) { Norma43::Utils::Contexts.new(
9
9
  [
@@ -22,7 +22,7 @@ module Norma43
22
22
  end
23
23
 
24
24
  context "when AdditionalItem is called" do
25
- let(:fake_additional_item) { double "Models::AdditionalItem" }
25
+ let(:fake_additional_item) { instance_double(Models::AdditionalItem) }
26
26
  before do
27
27
  allow(Models::AdditionalItem).to receive(:new) { fake_additional_item }
28
28
  end
@@ -2,15 +2,12 @@
2
2
 
3
3
  module Norma43
4
4
  module LineProcessors
5
- RSpec.describe "DocumentEnd" do
6
- class Thing
7
- end
8
-
9
- let(:line) { double "Line", record_number: 35 }
5
+ RSpec.describe DocumentEnd do
6
+ let(:line) { instance_double(LineParsers::DocumentEnd, record_number: 35) }
10
7
 
11
8
  it "moves to the nearest document context" do
12
9
  document = Norma43::Models::Document.new
13
- contexts = Norma43::Utils::Contexts.new [Thing.new, document, Thing.new, Thing.new]
10
+ contexts = Norma43::Utils::Contexts.new [anything, document, anything, anything]
14
11
 
15
12
  DocumentEnd.call line, contexts
16
13
 
@@ -2,8 +2,8 @@
2
2
 
3
3
  module Norma43
4
4
  module LineProcessors
5
- RSpec.describe "DocumentStart" do
6
- let (:line) { double "Line", attributes: {} }
5
+ RSpec.describe DocumentStart do
6
+ let(:line) { instance_double(LineParsers::DocumentStart, attributes: {}) }
7
7
 
8
8
  it "instantiates a new document with the line attributes" do
9
9
  allow(Models::Document).to receive :new
@@ -14,7 +14,7 @@ module Norma43
14
14
  end
15
15
 
16
16
  it "sets the document as the current context" do
17
- fake_document = double "Models::Document"
17
+ fake_document = instance_double(Models::Document)
18
18
  allow(Models::Document).to receive(:new) { fake_document }
19
19
 
20
20
  contexts = DocumentStart.call line, Norma43::Utils::Contexts.new
@@ -2,8 +2,8 @@
2
2
 
3
3
  module Norma43
4
4
  module LineProcessors
5
- RSpec.describe "Transaction" do
6
- let(:line) { double "Line", attributes: {} }
5
+ RSpec.describe Transaction do
6
+ let(:line) { instance_double(LineParsers::Transaction, attributes: {}) }
7
7
  let(:account) { Norma43::Models::Account.new }
8
8
  let(:contexts) { Norma43::Utils::Contexts.new(
9
9
  [
@@ -22,7 +22,7 @@ module Norma43
22
22
  end
23
23
 
24
24
  context "when Transaction is called" do
25
- let(:fake_transaction) { double "Models::Transaction" }
25
+ let(:fake_transaction) { instance_double(Models::Transaction) }
26
26
  before do
27
27
  allow(Models::Transaction).to receive(:new) { fake_transaction }
28
28
  end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Norma43
4
+ module Models
5
+ class Account
6
+ RSpec.describe SpanishIban do
7
+ def build_account(**attributes)
8
+ instance_double(Account, **attributes)
9
+ end
10
+
11
+ describe ".from_account" do
12
+ subject(:iban) { described_class.from_account(account) }
13
+
14
+ context "with the example in the documentation" do
15
+ let(:account) { build_account(bank_code: 81, branch_code: 54, account_number: 1234567) }
16
+ it { is_expected.to eq("ES5400810054180001234567") }
17
+ end
18
+
19
+ context "with the account from the example1.n43 fixture" do
20
+ let(:account) {
21
+ file.seek 2
22
+ bank_code = file.read(4).to_i
23
+ branch_code = file.read(4).to_i
24
+ account_number = file.read(10).to_i
25
+
26
+ build_account(bank_code: bank_code, branch_code: branch_code, account_number: account_number)
27
+ }
28
+ let(:file) { File.open(File.join(__dir__, "../../../fixtures/example1.n43"), encoding: "iso-8859-1") }
29
+
30
+ it { is_expected.to eq("ES1799991111710123456789") }
31
+
32
+ after { file.close }
33
+ end
34
+
35
+ context "with minimal edge-case" do
36
+ let(:account) { build_account(bank_code: 3, branch_code: 0, account_number: 0) }
37
+ it { is_expected.to eq("ES8700030000300000000000") }
38
+ end
39
+
40
+ context "with extreme edge-case" do
41
+ let(:account) { build_account(bank_code: 9000, branch_code: 9999, account_number: 9999999999) }
42
+ it { is_expected.to eq("ES3790009999309999999999") }
43
+ end
44
+ end
45
+
46
+ describe "#to_s" do
47
+ subject { described_class.new(bank_code: 0, branch_code: 0, account_number: 0) }
48
+
49
+ it { is_expected.to(respond_to(:to_s)) }
50
+
51
+ context "with incorrect account values" do
52
+ subject { described_class.new(bank_code: bank_code, branch_code: branch_code, account_number: account_number).to_s }
53
+
54
+ let(:bank_code) { 9999 }
55
+ let(:branch_code) { 1111 }
56
+ let(:account_number) { 123456789 }
57
+
58
+ context "with missing bank code" do
59
+ let(:bank_code) { nil }
60
+ it { is_expected.to be_empty }
61
+ end
62
+
63
+ context "with missing branch code" do
64
+ let(:branch_code) { nil }
65
+ it { is_expected.to be_empty }
66
+ end
67
+
68
+ context "with missing account number" do
69
+ let(:account_number) { nil }
70
+ it { is_expected.to be_empty }
71
+ end
72
+
73
+ context "with negative bank code" do
74
+ let(:bank_code) { -3 }
75
+ it { is_expected.to be_empty }
76
+ end
77
+
78
+ context "with negative branch code" do
79
+ let(:bank_code) { -1 }
80
+ it { is_expected.to be_empty }
81
+ end
82
+
83
+ context "with negative account number" do
84
+ let(:account_number) { -123456789 }
85
+ it { is_expected.to be_empty }
86
+ end
87
+
88
+ context "with more than 4 digits in the bank code" do
89
+ let(:bank_code) { 99990 }
90
+ it { is_expected.to be_empty }
91
+ end
92
+
93
+ context "with more than 4 digits in the branch code" do
94
+ let(:bank_code) { 11110 }
95
+ it { is_expected.to be_empty }
96
+ end
97
+
98
+ context "with more than 10 digits in the account number" do
99
+ let(:account_number) { 12345678900 }
100
+ it { is_expected.to be_empty }
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Norma43
4
+ module Models
5
+ RSpec.describe Account do
6
+ it_behaves_like "a model"
7
+
8
+ describe "#iban" do
9
+ it { is_expected.to respond_to(:iban) }
10
+
11
+ context "with the example in the documentation" do
12
+ it "returns the correct IBAN string" do
13
+ account = subject
14
+ account.account_number = 1234567
15
+ account.bank_code = 81
16
+ account.branch_code = 54
17
+
18
+ expect(account.iban).to eq("ES5400810054180001234567")
19
+ end
20
+ end
21
+
22
+ context "with missing bank data" do
23
+ subject { described_class.new.iban }
24
+ it { is_expected.to be_nil }
25
+ end
26
+
27
+ context "with missing bank code" do
28
+ subject { described_class.new(bank_code: nil, branch_code: 1234, account_number: 1234).iban }
29
+ it { is_expected.to be_nil }
30
+ end
31
+
32
+ context "with missing branch code" do
33
+ subject { described_class.new(bank_code: 1234, branch_code: nil, account_number: 1234).iban }
34
+ it { is_expected.to be_nil }
35
+ end
36
+
37
+ context "with missing account number" do
38
+ subject { described_class.new(bank_code: 1234, branch_code: 1234, account_number: nil).iban }
39
+ it { is_expected.to be_nil }
40
+ end
41
+
42
+ context "with negative account number" do
43
+ subject { described_class.new(bank_code: 1234, branch_code: 1234, account_number: -1234).iban }
44
+ it { is_expected.to be_nil }
45
+ end
46
+
47
+ context "with an account number which is too large" do
48
+ subject { described_class.new(bank_code: 1234, branch_code: 1234, account_number: 1*10**10).iban }
49
+ it { is_expected.to be_nil }
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Norma43
4
+ module Models
5
+ RSpec.describe Document do
6
+ it_behaves_like "a model"
7
+
8
+ describe "#transaction_date" do
9
+ it { is_expected.to respond_to(:transaction_date) }
10
+
11
+ context "when there are no accounts" do
12
+ subject(:document) { described_class.new({ accounts: [] }) }
13
+
14
+ it { expect(document.transaction_date).to be_nil }
15
+ end
16
+
17
+ context "when there are accounts" do
18
+ subject(:document) { described_class.new({ accounts: [account] }) }
19
+
20
+ let(:account) {
21
+ Account.new(transactions: [
22
+ nil,
23
+ Transaction.new(transaction_date: nil),
24
+ Transaction.new(transaction_date: Date.parse("2024-01-23")),
25
+ ])
26
+ }
27
+
28
+ it "returns the date of the first account with a transaction date" do
29
+ expect(document.transaction_date).to eq(Date.parse("2024-01-23"))
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Norma43
4
+ module Models
5
+ RSpec.describe Transaction do
6
+ it_behaves_like "a model"
7
+
8
+ describe "#debit?" do
9
+ it { is_expected.to respond_to(:debit?) }
10
+
11
+ context "when amount code is code for debits" do
12
+ subject(:transaction) { described_class.new(amount_code: 1) }
13
+
14
+ it { expect(transaction.debit?).to be_truthy }
15
+ end
16
+
17
+ context "when amount code is code for credits" do
18
+ subject(:transaction) { described_class.new(amount_code: 2) }
19
+
20
+ it { expect(transaction.debit?).to be_falsey }
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
data/spec/norma43_spec.rb CHANGED
@@ -6,7 +6,7 @@ RSpec.describe Norma43 do
6
6
  describe "#parse" do
7
7
  it "returns the parser results" do
8
8
  text = "some total-in text"
9
- parser = double "Parser", result: "result"
9
+ parser = instance_double(Norma43::Parser, result: "result")
10
10
  expect(Norma43::Parser).to receive(:new).with(text) { parser }
11
11
 
12
12
  expect(Norma43.parse(text)).to eq "result"
data/spec/spec_helper.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "norma43"
4
+
3
5
  RSpec.configure do |config|
4
6
  config.order = :random
5
7
  Kernel.srand config.seed
@@ -11,6 +13,7 @@ RSpec.configure do |config|
11
13
  config.mock_with :rspec do |mocks|
12
14
  mocks.syntax = :expect
13
15
  mocks.verify_partial_doubles = true
16
+ mocks.verify_doubled_constant_names = true
14
17
  end
15
18
  end
16
19
 
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.shared_examples "a model" do
4
+ it { is_expected.to respond_to(:attributes) }
5
+ it { is_expected.to respond_to(:attributes=) }
6
+ it { is_expected.to respond_to(:to_h) }
7
+ it { is_expected.to respond_to(:to_hash) }
8
+
9
+ describe "#new" do
10
+ subject(:model) { described_class.new(attributes) }
11
+
12
+ context "when instantiated with an empty hash" do
13
+ let(:attributes) { {} }
14
+
15
+ it "accepts it without failing" do
16
+ expect { model }.not_to raise_error
17
+ end
18
+ end
19
+ end
20
+
21
+ describe "#attributes" do
22
+ subject { described_class.new.attributes }
23
+
24
+ it { is_expected.not_to be_empty }
25
+ it { is_expected.to respond_to(:each_pair) }
26
+ it { is_expected.to respond_to(:to_h) }
27
+ it { is_expected.to respond_to(:to_hash) }
28
+ it { is_expected.to respond_to(:keys) }
29
+
30
+ describe "#attributes.keys" do
31
+ subject { super().keys }
32
+
33
+ it { is_expected.not_to be_empty }
34
+ it { is_expected.to all(be_a(Symbol)) }
35
+ end
36
+ end
37
+
38
+ describe "#attributes=" do
39
+ subject { described_class.new.attributes=(attributes) }
40
+
41
+ context "when passed an empty hash" do
42
+ let(:attributes) { {} }
43
+
44
+ it "accepts it without failing" do
45
+ expect { subject }.not_to raise_error
46
+ end
47
+ end
48
+
49
+ context "when passed a hash with unknown attribute names" do
50
+ let(:attributes) { { potato: nil } }
51
+
52
+ it "accepts it without failing" do
53
+ expect { subject }.not_to raise_error
54
+ end
55
+
56
+ it "does not set any new method" do
57
+ expect { subject.potato }.to raise_error(NoMethodError, /undefined method.+potato/i)
58
+ end
59
+ end
60
+ end
61
+ end