diff --git a/Gemfile b/Gemfile index 7277a8a6..89735e5e 100644 --- a/Gemfile +++ b/Gemfile @@ -6,6 +6,7 @@ ruby "3.3.2" gem "activesupport" gem "camt_parser", git: "https://github.com/railslove/camt_parser.git" +gem "sepa_file_parser" gem "cmxl", git: "https://github.com/railslove/cmxl" gem "epics" gem "faraday" diff --git a/Gemfile.lock b/Gemfile.lock index 3e68483d..d966b6e3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -55,6 +55,7 @@ GEM database_cleaner-sequel (2.0.2) database_cleaner-core (~> 2.0.0) sequel + date (3.4.1) diff-lcs (1.5.1) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) @@ -210,6 +211,10 @@ GEM sentry-sidekiq (5.22.1) sentry-ruby (~> 5.22.1) sidekiq (>= 3.0) + sepa_file_parser (0.4.0) + bigdecimal + nokogiri + time sepa_king (0.12.0) activemodel (>= 3.1) iban-tools @@ -239,6 +244,8 @@ GEM rubocop-performance (~> 1.21.0) statsd-ruby (1.5.0) tilt (2.4.0) + time (0.4.1) + date timecop (0.9.10) tzinfo (2.0.6) concurrent-ruby (~> 1.0) @@ -287,6 +294,7 @@ DEPENDENCIES rubocop sentry-ruby sentry-sidekiq + sepa_file_parser sepa_king sequel sidekiq diff --git a/Rakefile b/Rakefile index 8c298479..7b5adf32 100644 --- a/Rakefile +++ b/Rakefile @@ -86,7 +86,7 @@ namespace :migration_tasks do bank_statements = Box::BankStatement.where(account_id: account_id).all bank_statements.each do |bank_statement| - parser = bank_statement.content.starts_with?(":") ? Cmxl : CamtParser::Format053::Statement + parser = bank_statement.content.starts_with?(":") ? Cmxl : SepaFileParser::Camt053::Statement begin result = parser.parse(bank_statement.content) transactions = result.is_a?(Array) ? result.first.transactions : result.transactions diff --git a/box/business_processes/import_bank_statement.rb b/box/business_processes/import_bank_statement.rb index 7f2f7c5f..c675aa80 100644 --- a/box/business_processes/import_bank_statement.rb +++ b/box/business_processes/import_bank_statement.rb @@ -57,7 +57,7 @@ def self.find_or_create_bank_statement(raw_bank_statement, account) bs.account_id = account.id bs.sequence = raw_bank_statement.sequence bs.year = extract_year_from_bank_statement(raw_bank_statement) - bs.remote_account = raw_bank_statement.account_identification.source + bs.remote_account = raw_bank_statement.account_identification.iban bs.opening_balance = as_big_decimal(raw_bank_statement.opening_or_intermediary_balance) # this will be final or intermediate bs.closing_balance = as_big_decimal(raw_bank_statement.closing_or_intermediary_balance) # this will be final or intermediate bs.transaction_count = raw_bank_statement.transactions.count @@ -85,7 +85,7 @@ def self.as_big_decimal(input) def self.extract_year_from_bank_statement(raw_bank_statement) first_transaction = raw_bank_statement.transactions.first - first_transaction&.date&.year + first_transaction.date&.year end def self.checksum(raw_bank_statement, account) diff --git a/box/business_processes/import_statements.rb b/box/business_processes/import_statements.rb index f1b3cc01..fb136948 100644 --- a/box/business_processes/import_statements.rb +++ b/box/business_processes/import_statements.rb @@ -1,23 +1,17 @@ # frozen_string_literal: true -require "cmxl" -require "camt_parser" +require "sepa_file_parser" require_relative "../models/account" require_relative "../models/bank_statement" require_relative "../models/event" require_relative "../../lib/checksum_generator" +require_relative "../../lib/data_mapping/statement_factory" module Box module BusinessProcesses class ImportStatements - PARSERS = {"mt940" => Cmxl, "camt53" => CamtParser::Format053::Statement}.freeze - - def self.parse_bank_statement(bank_statement) - parser = PARSERS.fetch(bank_statement.account.statements_format, Cmxl) - result = parser.parse(bank_statement.content) - result.is_a?(Array) ? result.first.transactions : result.transactions - end + PARSERS = {"mt940" => Cmxl, "camt53" => SepaFileParser::Camt053::Statement}.freeze def self.from_bank_statement(bank_statement, upcoming = false) bank_transactions = parse_bank_statement(bank_statement) @@ -31,6 +25,14 @@ def self.from_bank_statement(bank_statement, upcoming = false) stats end + def self.parse_bank_statement(bank_statement) + parser = PARSERS.fetch(bank_statement.account.statements_format, Cmxl) + result = parser.parse(bank_statement.content) + statement_data = result.is_a?(Array) ? result.first : result + statement = DataMapping::StatementFactory.new(statement_data, bank_statement.account).call + statement.transactions + end + def self.create_statement(bank_statement, bank_transaction, upcoming = false) account = bank_statement.account trx = statement_attributes_from_bank_transaction(bank_transaction, bank_statement) @@ -66,20 +68,17 @@ def self.link_statement_to_transaction(account, statement) end def self.checksum(transaction, bank_statement) - ChecksumGenerator.from_payload(checksum_attributes(transaction, bank_statement.remote_account)) + checksum_payload = checksum_attributes(transaction, bank_statement.remote_account) + ChecksumGenerator.from_payload(checksum_payload) end def self.checksum_attributes(transaction, remote_account) - return [remote_account, transaction.transaction_id] if transaction.try(:transaction_id).present? + return [remote_account, transaction.transaction_id] if transaction.transaction_id.present? payload_from_transaction_attributes(transaction, remote_account) end def self.payload_from_transaction_attributes(transaction, remote_account) - eref = transaction.respond_to?(:eref) ? transaction.eref : transaction.sepa["EREF"] - mref = transaction.respond_to?(:mref) ? transaction.mref : transaction.sepa["MREF"] - svwz = transaction.respond_to?(:svwz) ? transaction.svwz : transaction.sepa["SVWZ"] - [ remote_account, transaction.date, @@ -87,34 +86,34 @@ def self.payload_from_transaction_attributes(transaction, remote_account) transaction.iban, transaction.name, transaction.sign, - eref, - mref, - svwz, + transaction.eref, + transaction.mref, + transaction.svwz, transaction.information.gsub(/\s/, "") ] end def self.statement_attributes_from_bank_transaction(transaction, bank_statement) { - sha: checksum(transaction, bank_statement), - date: transaction.date, - entry_date: transaction.entry_date, amount: transaction.amount_in_cents, - sign: transaction.sign, - debit: transaction.debit?, - swift_code: transaction.swift_code, - reference: transaction.reference, bank_reference: transaction.bank_reference, bic: transaction.bic, + creditor_identifier: transaction.creditor_identifier, + date: transaction.date, + debit: transaction.debit?, + description: transaction.description, + entry_date: transaction.entry_date, + eref: transaction.eref, iban: transaction.iban, - name: transaction.name, information: transaction.information, - description: transaction.description, - eref: transaction.respond_to?(:eref) ? transaction.eref : transaction.sepa["EREF"], - mref: transaction.respond_to?(:mref) ? transaction.mref : transaction.sepa["MREF"], - svwz: transaction.respond_to?(:svwz) ? transaction.svwz : transaction.sepa["SVWZ"], - tx_id: transaction.try(:primanota) || transaction.try(:transaction_id), - creditor_identifier: transaction.respond_to?(:creditor_identifier) ? transaction.creditor_identifier : transaction.sepa["CRED"] + mref: transaction.mref, + name: transaction.name, + reference: transaction.reference, + sha: checksum(transaction, bank_statement), + sign: transaction.sign, + svwz: transaction.svwz, + swift_code: transaction.swift_code, + tx_id: transaction.transaction_id } end end diff --git a/box/jobs/fetch_statements.rb b/box/jobs/fetch_statements.rb index a5991625..6c28645c 100644 --- a/box/jobs/fetch_statements.rb +++ b/box/jobs/fetch_statements.rb @@ -2,7 +2,7 @@ require "sidekiq-scheduler" require "active_support/all" -require "camt_parser" +require "sepa_file_parser" require "cmxl" require "epics" require "sequel" @@ -84,7 +84,7 @@ def camt53(client, from, to) combined_camt = client.C53(from.to_s(:db), to.to_s(:db)) return unless combined_camt.any? - combined_camt.map { |chunk| CamtParser::String.parse(chunk).statements }.flatten + combined_camt.map { |chunk| SepaFileParser::String.parse(chunk).statements }.flatten end def mt940(client, from, to) diff --git a/box/jobs/fetch_upcoming_statements.rb b/box/jobs/fetch_upcoming_statements.rb index 84408782..c0189604 100644 --- a/box/jobs/fetch_upcoming_statements.rb +++ b/box/jobs/fetch_upcoming_statements.rb @@ -2,7 +2,7 @@ require "sidekiq-scheduler" require "active_support/all" -require "camt_parser" +require "sepa_file_parser" require "cmxl" require "epics" require "sequel" diff --git a/box/jobs/queue_fetch_statements.rb b/box/jobs/queue_fetch_statements.rb index d50d4ea9..867401af 100644 --- a/box/jobs/queue_fetch_statements.rb +++ b/box/jobs/queue_fetch_statements.rb @@ -2,7 +2,7 @@ require "sidekiq-scheduler" require "active_support/all" -require "camt_parser" +require "sepa_file_parser" require "cmxl" require "epics" require "sequel" diff --git a/box/jobs/queue_fetch_upcoming_statements.rb b/box/jobs/queue_fetch_upcoming_statements.rb index bdfa8299..af58ad8c 100644 --- a/box/jobs/queue_fetch_upcoming_statements.rb +++ b/box/jobs/queue_fetch_upcoming_statements.rb @@ -2,7 +2,7 @@ require "sidekiq-scheduler" require "active_support/all" -require "camt_parser" +require "sepa_file_parser" require "cmxl" require "epics" require "sequel" diff --git a/lib/data_mapping/camt53/statement.rb b/lib/data_mapping/camt53/statement.rb index 9b3b70b9..622ce7e8 100644 --- a/lib/data_mapping/camt53/statement.rb +++ b/lib/data_mapping/camt53/statement.rb @@ -1,3 +1,5 @@ +require_relative "transaction" + module DataMapping module Camt53 class Statement @@ -12,11 +14,11 @@ def blank? end def account_identification - raw_bank_statement.account_identification + raw_bank_statement.account end def closing_or_intermediary_balance - raw_bank_statement.closing_or_intermediary_balance + raw_bank_statement.closing_balance end def sequence @@ -24,7 +26,7 @@ def sequence end def opening_or_intermediary_balance - raw_bank_statement.closing_or_intermediary_balance + raw_bank_statement.opening_balance end def source @@ -32,7 +34,9 @@ def source end def transactions - raw_bank_statement.transactions + raw_bank_statement.entries.map do |entry| + Transaction.new(entry) + end end end end diff --git a/lib/data_mapping/camt53/transaction.rb b/lib/data_mapping/camt53/transaction.rb new file mode 100644 index 00000000..b5bc7d56 --- /dev/null +++ b/lib/data_mapping/camt53/transaction.rb @@ -0,0 +1,73 @@ +module DataMapping + module Camt53 + class Transaction + attr_reader :raw_bank_statement, :first_transaction + + delegate :amount_in_cents, + :bank_reference, + :credit?, + :debit?, + :reference, + :sign, + :transaction_id, + to: :raw_bank_statement + + def initialize(raw_bank_statement) + @raw_bank_statement = raw_bank_statement + @first_transaction = raw_bank_statement.transactions.first + end + + def bic + first_transaction.bic + end + + def creditor_identifier + first_transaction.creditor_identifier + end + + def date + raw_bank_statement.value_date + end + + def description + raw_bank_statement.additional_information + end + + def entry_date + raw_bank_statement.booking_date + end + + def eref + first_transaction.end_to_end_reference + end + + def iban + first_transaction.iban + end + + def information + first_transaction.payment_information + end + + def mref + first_transaction.mandate_reference + end + + def name + first_transaction.name + end + + def svwz + first_transaction.remittance_information + end + + def swift_code + first_transaction.swift_code + end + + def transaction_id + first_transaction.transaction_id + end + end + end +end diff --git a/lib/data_mapping/cmxl/account.rb b/lib/data_mapping/cmxl/account.rb new file mode 100644 index 00000000..66a2e46c --- /dev/null +++ b/lib/data_mapping/cmxl/account.rb @@ -0,0 +1,19 @@ +module DataMapping + module Cmxl + class Account + attr_reader :raw_bank_statement + + delegate :account_number, + to: :raw_bank_statement + + def initialize(raw_bank_statement) + @raw_bank_statement = raw_bank_statement + end + + def iban + return raw_bank_statement.iban if raw_bank_statement.iban.present? + raw_bank_statement.source + end + end + end +end diff --git a/lib/data_mapping/cmxl/statement.rb b/lib/data_mapping/cmxl/statement.rb index d39d773d..23e92d95 100644 --- a/lib/data_mapping/cmxl/statement.rb +++ b/lib/data_mapping/cmxl/statement.rb @@ -1,14 +1,16 @@ +require_relative "account" +require_relative "transaction" + module DataMapping module Cmxl class Statement attr_reader :raw_bank_statement - delegate :account_identification, + delegate :account_number, :blank?, :closing_or_intermediary_balance, :opening_or_intermediary_balance, :source, - :transactions, to: :raw_bank_statement def initialize(raw_bank_statement) @@ -18,6 +20,16 @@ def initialize(raw_bank_statement) def sequence raw_bank_statement.legal_sequence_number end + + def account_identification + Account.new(raw_bank_statement.account_identification) + end + + def transactions + raw_bank_statement.transactions.map do |entry| + Transaction.new(entry) + end + end end end end diff --git a/lib/data_mapping/cmxl/transaction.rb b/lib/data_mapping/cmxl/transaction.rb new file mode 100644 index 00000000..6e2e649c --- /dev/null +++ b/lib/data_mapping/cmxl/transaction.rb @@ -0,0 +1,49 @@ +module DataMapping + module Cmxl + class Transaction + attr_reader :raw_bank_statement + + delegate :amount_in_cents, + :date, + :information, + :name, + :sepa, + :sign, + :bank_reference, + :bic, + :iban, + :credit?, + :debit?, + :description, + :entry_date, + :reference, + :swift_code, + :remote_account, + to: :raw_bank_statement + + def initialize(raw_bank_statement) + @raw_bank_statement = raw_bank_statement + end + + def eref + raw_bank_statement.sepa["EREF"] + end + + def mref + raw_bank_statement.sepa["MREF"] + end + + def svwz + raw_bank_statement.sepa["SVWZ"] + end + + def transaction_id + raw_bank_statement.primanota + end + + def creditor_identifier + raw_bank_statement.sepa["CRED"] + end + end + end +end diff --git a/spec/business_processes/import_bank_statement_spec.rb b/spec/business_processes/import_bank_statement_spec.rb index d9f30028..cb9a7493 100644 --- a/spec/business_processes/import_bank_statement_spec.rb +++ b/spec/business_processes/import_bank_statement_spec.rb @@ -86,12 +86,12 @@ def import(cmxl, account) describe "camt statements" do it "creates a new bank statement from camt" do - c53 = CamtParser::String.parse(camt).statements.first + c53 = SepaFileParser::String.parse(camt).statements.first expect { import(c53, camt_account) }.to(change(BankStatement, :count).by(1)) end it "extracts a date for bank statements" do - c53 = CamtParser::String.parse(camt).statements.first + c53 = SepaFileParser::String.parse(camt).statements.first expect(import(c53, camt_account).year).to eq(2013) end end diff --git a/spec/business_processes/import_statement_spec.rb b/spec/business_processes/import_statement_spec.rb index f6a2c1cd..db34fbb7 100644 --- a/spec/business_processes/import_statement_spec.rb +++ b/spec/business_processes/import_statement_spec.rb @@ -25,106 +25,96 @@ module BusinessProcesses described_class.from_bank_statement(bank_statement) end - # TODO: Check with @namxam why this was done in the first place - # context 'identical consecutive entries in bank statements' do - # let(:mt940) { File.read('spec/fixtures/duplicated_entries.mt940') } - - # it 'imports both entries' do - # bank_statement = ImportBankStatement.from_mt940(mt940, account) - # expect { described_class.from_bank_statement(bank_statement) }.to change(Statement, :count).by(2) - # end - # end - - describe ".create_statement" do - let(:bank_statement) do - double( - id: 42, - account: account, - remote_account: "FooBar/4711", - sequence: "47/11", - opening_balance: 123, - closing_balance: 456, - fetched_on: "2015-06-20" + context "statement is new" do + it "extracts subdata from sepa subtree" do + mt940_bank_statement = Fabricate(:mt940_statement) + described_class.from_bank_statement(mt940_bank_statement, true) + + expect(Statement.last.values).to match( + hash_including( + eref: "00002266010540060117153121", + mref: "054874", + svwz: "GRUNSGLOB//SPARKASSE/DE 06-01-2017T15:31:21 FOLGENR. 09 VERFALLD. 1220 FREMDENTGELT 4,00 EUR", + creditor_identifier: "DE1231231232501" + ) ) end - let(:data) do - double("MT940 Transaction", - date: "2015-06-20", - entry_date: "2015-06-20", - amount_in_cents: 100_24, - sign: 1, - debit?: true, - swift_code: "swift_code", - reference: "reference", - bank_reference: "bank_reference", - bic: "bic", - iban: "iban", - name: "name", - information: "information", - description: "description", - sha: "balbalblabladslflasdfk", - sepa: { - "EREF" => "my-eref", - "MREF" => "my-mref", - "SVWZ" => "my-svwz", - "CRED" => "my-cred" - }) + it "creates an event" do + expect(Event).to receive(:publish).with(:statement_created, anything) + mt940_bank_statement = Fabricate(:mt940_statement) + described_class.from_bank_statement(mt940_bank_statement) end + end - before do - allow(Account).to receive(:[]).and_return(double("account", organization: double("orga", webhook_token: "token"))) - end + context "when the statement was already imported" do + it "does not create a new record" do + # This is a precalculated SHA based on our algorithm - def exec_create_action - described_class.create_statement(bank_statement, data) + mt940_bank_statement = Fabricate(:mt940_statement) + Statement.create( + sha: "a7f3bb583423771042fd4ca70c5b10cb11afa9692387253f341cc83852962066", + account_id: mt940_bank_statement.account.id + ) + + expect { described_class.from_bank_statement(mt940_bank_statement) }.to_not change(Statement, :count) end + end - context "the statement was already imported" do - # This is a precalculated SHA based on our algorithm - before { Statement.create(sha: "964fa5bd7ffdd1614f46324177e14f3d4a19af18e1bb33e0c30b3e019d0323e8", account_id: account.id) } + describe "settled" do + context "when the final import is triggered" do + it "marks the statement as settled when the statement is already present" do + mt940_bank_statement = Fabricate(:mt940_statement) + Statement.create( + sha: "a7f3bb583423771042fd4ca70c5b10cb11afa9692387253f341cc83852962066", + account_id: mt940_bank_statement.account.id, + settled: false + ) + upcoming_flag = false - it "does not create a statement" do - expect { exec_create_action }.to_not change(Statement, :count) - end - end + described_class.from_bank_statement(mt940_bank_statement, upcoming_flag) - context "statement is new" do - it "extracts subdata from sepa subtree" do - exec_create_action - expect(Statement.last.values).to match(hash_including( - eref: "my-eref", - mref: "my-mref", - svwz: "my-svwz", - creditor_identifier: "my-cred" - )) + expect(Statement.last.settled).to be true end - it "creates an event" do - expect(Event).to receive(:publish).with(:statement_created, anything) - exec_create_action + it "marks the statement as settled when the statement is not present" do + mt940_bank_statement = Fabricate(:mt940_statement) + upcoming_flag = false + + described_class.from_bank_statement(mt940_bank_statement, upcoming_flag) + + expect(Statement.last.settled).to be true end end - end - describe "duplicated bank statement number" do - let!(:cmxl_2016) { File.read("spec/fixtures/duplicated_sequence_number_2016.mt940") } - let!(:cmxl_2017) { File.read("spec/fixtures/duplicated_sequence_number_2017.mt940") } + context "when the import is triggered as upcoming" do + it "does not mark the statement as settled" do + mt940_bank_statement = Fabricate(:mt940_statement) + upcoming_flag = true + + described_class.from_bank_statement(mt940_bank_statement, upcoming_flag) - it "imports even if statement number is duplicated" do - bank_statement = ImportBankStatement.from_mt940(cmxl_2016, account) - expect { described_class.from_bank_statement(bank_statement) }.to change { Statement.count }.by(4) - bank_statement = ImportBankStatement.from_mt940(cmxl_2017, account) - expect { described_class.from_bank_statement(bank_statement) }.to change { Statement.count }.by(2) + expect(Statement.last.settled).to be false + end end end + # TODO: Check with @namxam why this was done in the first place + # context 'identical consecutive entries in bank statements' do + # let(:mt940) { File.read('spec/fixtures/duplicated_entries.mt940') } + + # it 'imports both entries' do + # bank_statement = ImportBankStatement.from_mt940(mt940, account) + # expect { described_class.from_bank_statement(bank_statement) }.to change(Statement, :count).by(2) + # end + # end + describe "camt bank statement import" do context "with trx ids" do let(:camt) { File.read("spec/fixtures/camt_statement_with_trx_ids.xml") } it "imports camt statements" do - parsed_camt = CamtParser::String.parse(camt).statements + parsed_camt = SepaFileParser::String.parse(camt).statements bank_statement = ImportBankStatement.process(parsed_camt.first, camt_account) expect { described_class.from_bank_statement(bank_statement) }.to change { Statement.count }.by(4) end @@ -134,7 +124,7 @@ def exec_create_action let(:camt) { File.read("spec/fixtures/camt_statement.xml") } it "imports camt statements" do - parsed_camt = CamtParser::String.parse(camt).statements + parsed_camt = SepaFileParser::String.parse(camt).statements bank_statement = ImportBankStatement.process(parsed_camt.first, camt_account) expect { described_class.from_bank_statement(bank_statement) }.to change { Statement.count }.by(4) end @@ -142,7 +132,8 @@ def exec_create_action context "with trx ids" do let(:camt) { File.read("spec/fixtures/camt_statement_with_trx_ids.xml") } - let(:parsed_camt) { CamtParser::String.parse(camt).statements } + let(:parsed_camt) { SepaFileParser::String.parse(camt).statements } + let(:old_parsed_camt) { CamtParser::String.parse(camt).statements } let(:bank_statement) { ImportBankStatement.process(parsed_camt.first, camt_account) } subject { described_class.from_bank_statement(bank_statement) } @@ -201,63 +192,11 @@ def exec_link_action let(:mt940b) { File.read("spec/fixtures/dup_whitespace_transaction.mt940") } let(:bank_statement) { ImportBankStatement.from_mt940(mt940, account) } - it "importes both statements" do - expect { described_class.from_bank_statement(bank_statement) }.to( - change(Statement, :count).by(2) - ) - end - - it "recognizes duplicates when importing data again" do - expect do - described_class.from_bank_statement(bank_statement) - described_class.from_bank_statement(bank_statement) - end.to(change(Statement, :count).by(2)) - end - it "does not import same transaction with different whitespaces in reference" do bank_statement = ImportBankStatement.from_mt940(mt940b, account) expect { described_class.from_bank_statement(bank_statement) }.to change(Statement, :count).by(1) end end - - describe "importing VMK" do - let(:mt942) { File.read("spec/fixtures/single_valid.mt942") } - let(:bank_statement) { ImportBankStatement.from_mt940(mt942, account) } - let(:statement) { Box::Statement.first(sha: "be3a41c6262f85b409fefee10d7515893301411c765ffbf708a36694a365c213") } - - it "imports each statement" do - expect(described_class).to receive(:create_statement).once - described_class.from_bank_statement(bank_statement, upcoming: true) - end - - it "marks statements as unsettled" do - described_class.from_bank_statement(bank_statement, upcoming: true) - expect(statement.settled).to be_falsey - end - - context "importing mt940 statement that was previously imported via vmk" do - let(:mt940_bank_statement) { ImportBankStatement.from_mt940(mt940, account) } - let(:mt940_bank_transactions) { described_class.parse_bank_statement(mt940_bank_statement) } - - before { described_class.from_bank_statement(bank_statement, upcoming: true) } - - it "does not create a new statement" do - transaction = mt940_bank_transactions.first - expect(described_class.create_statement(mt940_bank_statement, transaction)).to be_falsey - end - - it "does update statement from VMK to be settled" do - transaction = mt940_bank_transactions.first - described_class.create_statement(mt940_bank_statement, transaction) - expect(statement.settled).to be_truthy - end - - it "still imports remaining statements" do - transaction = mt940_bank_transactions.last - expect(described_class.create_statement(mt940_bank_statement, transaction)).to be_truthy - end - end - end end end end diff --git a/spec/fabricators/bank_statement_fabricator.rb b/spec/fabricators/bank_statement_fabricator.rb new file mode 100644 index 00000000..304b5514 --- /dev/null +++ b/spec/fabricators/bank_statement_fabricator.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require_relative "../../box/models/bank_statement" + +Fabricator(:bank_statement, from: "Box::BankStatement") do + account { Fabricate(:account) } + fetched_on { Date.today } + sha { Faker::Crypto.sha256 } +end + +Fabricator(:camt_statement, from: :bank_statement) do + sequence { "130000005" } + year { 2013 } + remote_account { "iban1234567" } + opening_balance { 33.06 } + closing_balance { 23.06 } + transaction_count { 4 } + content { File.read("spec/fixtures/camt/statement-part.xml") } +end + +Fabricator(:mt940_statement, from: :bank_statement) do + sequence { "00006/004" } + year { 2017 } + remote_account { "10020030/1234567" } + opening_balance { 57868.58 } + closing_balance { 57839.48 } + transaction_count { 2 } + content { File.read("spec/fixtures/single_valid_swift.mt940") } +end diff --git a/spec/fixtures/camt/statement-part.xml b/spec/fixtures/camt/statement-part.xml new file mode 100644 index 00000000..daf6da01 --- /dev/null +++ b/spec/fixtures/camt/statement-part.xml @@ -0,0 +1,298 @@ + + 0352C5320131227220503 + 130000005 + 2013-12-27T22:04:52.0+01:00 + + + iban1234567 + + EUR + + Testkonto Nummer 1 + + + + GENODEF1PFK + VR-Bank Rottal-Inn eG + + DE 129267947 + UmsStId + + + + + + + + PRCD + + + 33.06 + CRDT +
+
2013-12-27
+ +
+ + + + CLBD + + + 23.06 + CRDT +
+
2013-12-27
+ +
+ + 2.00 + DBIT + BOOK + +
2013-12-27
+
+ +
2013-12-27
+
+ 2013122710583450000 + + + + + + NTRF+020 + ZKA + + + + + Testkonto Nummer 2 + + + + + 740618130100033626 + + BBAN + + + + + + + TEST BERWEISUNG MITTELS BLZUND KONTONUMMER - DTA + + + +
+ + 3.00 + DBIT + BOOK + +
2013-12-27
+
+ +
2013-12-27
+
+ 2013122710583600000 + + + + + CCTI/VRNWSW/b044f24cddb92a502b8a1b5 + NOTPROVIDED + + + + NMSC+201 + ZKA + + + + + Testkonto Nummer 1 + + + + DE14740618130000033626 + + + + keine Information vorhanden + + + Testkonto Nummer 2 + + + + DE58740618130100033626 + + + + keine Information vorhanden + + + + + + GENODEF1PFK + + + + + Test+berweisung mit BIC und IBAN SEPA IBAN: DE58740618130100033626 BIC: GENODEF1PFK + + + +
+ + 1.00 + CRDT + BOOK + +
2013-12-27
+
+ +
2013-12-27
+
+ 2013122711085260000 + + + + + + NMSC+051 + ZKA + + + + + Testkonto Nummer 2 + + + + + 740618130100033626 + + BBAN + + + + + + + R CKBUCHUNG + + + +
+ + 6.00 + DBIT + BOOK + +
2013-12-27
+
+ +
2013-12-27
+
+ 2013122711513230000 + + + + STZV-PmInf27122013-11:02-2 + 2 + + + + STZV-Msg27122013-11:02 + STZV-EtE27122013-11:02-1 + + + + 3.50 + + + + + NMSC+201 + ZKA + + + + + Testkonto Nummer 2 + + + + DE58740618130100033626 + + + + keine Information vorhanden + + + Testkonto Nummer 1 + + + + DE14740618130000033626 + + + + Testkonto + + + + Sammelueberwseisung 2. Zahlung TAN:283044 + + + + + STZV-Msg27122013-11:02 + STZV-EtE27122013-11:02-2 + + + + 2.50 + + + + + NMSC+201 + ZKA + + + + + Testkonto Nummer 2 + + + + DE58740618130100033626 + + + + keine Information vorhanden + + + Testkonto Nummer 1 + + + + DE14740618130000033626 + + + + Testkonto + + + + Sammelueberweisung 1. Zahlung TAN:283044 + + + +
+
\ No newline at end of file diff --git a/spec/fixtures/single_valid_swift.mt940 b/spec/fixtures/single_valid_swift.mt940 new file mode 100644 index 00000000..0fff83fd --- /dev/null +++ b/spec/fixtures/single_valid_swift.mt940 @@ -0,0 +1,12 @@ +:20:1234567 +:25:PL25106000760000888888888888 +:28C:00006/004 +:60M:C170109EUR57868,58 +:61:170109D29,00NDDTNONREF//PK17000010768140 +:86:106?00VERFUEGUNG GELDAUTOMAT?109075/659?20EREF+000022660105400601 +1715?213121?22MREF+054874?23CRED+DE1231231232501?24SVWZ+GRUNS +GLOB//SPARKASSE?25/DE 06-01-2017T?2615:31:21 FOLGENR +. 09 VERF?27ALLD. 1220 FREMDENTGELT 4,?2800 EUR?30MALADE51DKH?3 +1DE31231231231232309?32SPARKASSE RHEIN-HAARDT?34023 +:62F:C170109EUR57839,48 +:64:C170109EUR57839,48 diff --git a/spec/lib/data_mapping/camt53/statement_spec.rb b/spec/lib/data_mapping/camt53/statement_spec.rb index c9e2b582..1028c0f1 100644 --- a/spec/lib/data_mapping/camt53/statement_spec.rb +++ b/spec/lib/data_mapping/camt53/statement_spec.rb @@ -5,7 +5,7 @@ RSpec.describe DataMapping::Camt53::Statement do let(:raw_bank_statement) do camt_file = File.read("spec/fixtures/camt_statement.xml") - CamtParser::String.parse(camt_file).statements.first + SepaFileParser::String.parse(camt_file).statements.first end let(:statement) { described_class.new(raw_bank_statement) } @@ -28,8 +28,8 @@ expect(statement.account_identification.account_number).to be_instance_of(String) end - it "returns an object with a source method" do - expect(statement.account_identification).to respond_to(:source) + it "returns an object with a iban method" do + expect(statement.account_identification.iban).to be_instance_of(String) end end @@ -75,7 +75,7 @@ describe "#transactions" do it "returns the transactions from raw_bank_statement" do - expect(statement.transactions).not_to be_nil + expect(statement.transactions).to all(be_instance_of(DataMapping::Camt53::Transaction)) end end end diff --git a/spec/lib/data_mapping/camt53/transaction_spec.rb b/spec/lib/data_mapping/camt53/transaction_spec.rb new file mode 100644 index 00000000..f720148a --- /dev/null +++ b/spec/lib/data_mapping/camt53/transaction_spec.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +require_relative "../../../../lib/data_mapping/camt53/transaction" + +RSpec.describe DataMapping::Camt53::Transaction do + let(:raw_bank_statement) do + camt_file = File.read("spec/fixtures/camt_statement.xml") + statement = SepaFileParser::String.parse(camt_file).statements.first + statement.entries.first + end + let(:transaction) { described_class.new(raw_bank_statement) } + + describe "delegated methods" do + it "delegates bank_reference to raw_bank_statement" do + expect(transaction.amount_in_cents).to be_instance_of(Integer) + end + + it "delegates bank_reference to raw_bank_statement" do + expect(transaction.bank_reference).to be_instance_of(String) + end + + it "delegates credit? to raw_bank_statement" do + expect(transaction.credit?).to be(true).or be(false) + end + + it "delegates debit? to raw_bank_statement" do + expect(transaction.debit?).to be(true).or be(false) + end + + it "delegates reference to raw_bank_statement" do + expect(transaction.reference).to be_instance_of(String) + end + + it "delegates sign to raw_bank_statement" do + expect(transaction.sign).to be_instance_of(Integer) + end + end + + describe "#bic" do + it "returns the BIC" do + expect(transaction.bic).to be_instance_of(String) + end + end + + describe "#creditor_identifier" do + it "returns the creditor identifier" do + expect(transaction.creditor_identifier).to be_instance_of(String) + end + end + + describe "#date" do + it "returns the value date" do + expect(transaction.date).to be_instance_of(Date) + end + end + + describe "#description" do + it "returns the additional information" do + expect(transaction.description).to be_instance_of(String) + end + end + + describe "#entry_date" do + it "returns the booking date" do + expect(transaction.entry_date).to be_instance_of(Date) + end + end + + describe "#eref" do + it "returns the end to end reference" do + expect(transaction.eref).to be_instance_of(String) + end + end + + describe "#iban" do + it "returns the IBAN" do + expect(transaction.iban).to be_instance_of(String) + end + end + + describe "#information" do + it "returns the payment information" do + expect(transaction.information).to be_instance_of(String) + end + end + + describe "#mref" do + it "returns the mandate reference" do + expect(transaction.mref).to be_instance_of(String) + end + end + + describe "#name" do + it "returns the name" do + expect(transaction.name).to be_instance_of(String) + end + end + + describe "#svwz" do + it "returns the remittance information" do + expect(transaction.svwz).to be_instance_of(String) + end + end + + describe "#swift_code" do + it "returns the swift code" do + expect(transaction.swift_code).to be_instance_of(String) + end + end + + describe "#transaction_id" do + it "returns the transaction ID" do + expect(transaction.transaction_id).to be_instance_of(String) + end + end +end diff --git a/spec/lib/data_mapping/cmxl/account_spec.rb b/spec/lib/data_mapping/cmxl/account_spec.rb new file mode 100644 index 00000000..1dff28e6 --- /dev/null +++ b/spec/lib/data_mapping/cmxl/account_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require_relative "../../../../lib/data_mapping/cmxl/account" + +RSpec.describe DataMapping::Cmxl::Account do + let(:cmxl_file) { File.read("spec/fixtures/single_valid.mt940") } + let(:raw_bank_statement) do + statement = Cmxl.parse(cmxl_file).first + statement.account_identification + end + let(:account) { described_class.new(raw_bank_statement) } + + describe "delegated methods" do + it "delegates account_number to raw_bank_statement" do + expect(account.account_number).to be_instance_of(String) + end + end + + describe "#iban" do + context "when IBAN is present" do + let(:cmxl_file) { File.read("spec/fixtures/single_valid_swift.mt940") } + it "returns the IBAN" do + expect(account.iban).to be_instance_of(String) + end + end + + context "when IBAN is not present" do + # let(:cmxl_file) { File.read("spec/fixtures/single_valid_2016-03-15.mt940") } + + it "returns the source" do + expect(account.iban).to eq(raw_bank_statement.source) + end + end + end +end diff --git a/spec/lib/data_mapping/cmxl/statement_spec.rb b/spec/lib/data_mapping/cmxl/statement_spec.rb index c7f04af7..c80d987b 100644 --- a/spec/lib/data_mapping/cmxl/statement_spec.rb +++ b/spec/lib/data_mapping/cmxl/statement_spec.rb @@ -21,11 +21,7 @@ describe "#account_identification" do it "returns an object with an account_number method" do - expect(statement.account_identification.account_number).to be_instance_of(String) - end - - it "returns an object with a source method" do - expect(statement.account_identification).to respond_to(:source) + expect(statement.account_identification).to be_instance_of(DataMapping::Cmxl::Account) end end @@ -71,7 +67,7 @@ describe "#transactions" do it "returns the transactions from raw_bank_statement" do - expect(statement.transactions).not_to be_nil + expect(statement.transactions).to all(be_instance_of(DataMapping::Cmxl::Transaction)) end end end diff --git a/spec/lib/data_mapping/cmxl/transaction_spec.rb b/spec/lib/data_mapping/cmxl/transaction_spec.rb new file mode 100644 index 00000000..2eca63a1 --- /dev/null +++ b/spec/lib/data_mapping/cmxl/transaction_spec.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true + +require_relative "../../../../lib/data_mapping/cmxl/transaction" + +RSpec.describe DataMapping::Cmxl::Transaction do + let(:cmxl_file) { File.read("spec/fixtures/single_valid.mt940") } + let(:raw_bank_statement) do + statement = Cmxl.parse(cmxl_file).first + statement.transactions.first + end + let(:transaction) { described_class.new(raw_bank_statement) } + + describe "delegated methods" do + it "delegates amount_in_cents to raw_bank_statement" do + expect(transaction.amount_in_cents).to be_instance_of(Integer) + end + + it "delegates bank_reference to raw_bank_statement" do + expect(transaction.bank_reference).to be_instance_of(String) + end + + it "delegates credit? to raw_bank_statement" do + expect(transaction.credit?).to be(true).or be(false) + end + + it "delegates debit? to raw_bank_statement" do + expect(transaction.debit?).to be(true).or be(false) + end + + it "delegates reference to raw_bank_statement" do + expect(transaction.reference).to be_instance_of(String) + end + + it "delegates sign to raw_bank_statement" do + expect(transaction.sign).to be_instance_of(Integer) + end + end + + describe "#bic" do + it "returns the BIC" do + expect(transaction.bic).to be_instance_of(String) + end + end + + describe "#creditor_identifier" do + let(:cmxl_file) { File.read("spec/fixtures/single_valid_swift.mt940") } + + it "returns the creditor identifier" do + expect(transaction.creditor_identifier).to be_instance_of(String) + end + end + + describe "#date" do + it "returns the value date" do + expect(transaction.date).to be_instance_of(Date) + end + end + + describe "#description" do + it "returns the additional information" do + expect(transaction.description).to be_instance_of(String) + end + end + + describe "#entry_date" do + it "returns the booking date" do + expect(transaction.entry_date).to be_instance_of(Date) + end + end + + describe "#eref" do + let(:cmxl_file) { File.read("spec/fixtures/single_valid_swift.mt940") } + + it "returns the end to end reference" do + expect(transaction.eref).to be_instance_of(String) + end + end + + describe "#iban" do + let(:cmxl_file) { File.read("spec/fixtures/single_valid_swift.mt940") } + + it "returns the IBAN" do + expect(transaction.iban).to be_instance_of(String) + end + end + + describe "#information" do + it "returns the payment information" do + expect(transaction.information).to be_instance_of(String) + end + end + + describe "#mref" do + let(:cmxl_file) { File.read("spec/fixtures/single_valid_swift.mt940") } + + it "returns the mandate reference" do + expect(transaction.mref).to be_instance_of(String) + end + end + + describe "#name" do + it "returns the name" do + expect(transaction.name).to be_instance_of(String) + end + end + + describe "#svwz" do + let(:cmxl_file) { File.read("spec/fixtures/single_valid_swift.mt940") } + + it "returns the remittance information" do + expect(transaction.svwz).to be_instance_of(String) + end + end + + describe "#swift_code" do + it "returns the swift code" do + expect(transaction.swift_code).to be_instance_of(String) + end + end + + describe "#transaction_id" do + it "returns the transaction ID" do + expect(transaction.transaction_id).to be_instance_of(String) + end + end +end