From f52466f4f462471336ac3601ca8c728b2c61e341 Mon Sep 17 00:00:00 2001 From: Tobias Schoknecht Date: Sun, 19 May 2024 14:30:46 +0200 Subject: [PATCH] Initial commit after fork Forked from github.com/viafintech/camt_parser --- .dockerignore | 3 + .github/dependabot.yml | 6 + .github/workflows/test.yml | 30 + .gitignore | 18 + .rspec | 2 + Gemfile | 4 + Gemfile.lock | 48 + LICENSE | 21 + READMDE.md | 55 + Rakefile | 11 + lib/sepa_file_parser.rb | 33 + lib/sepa_file_parser/camt052/base.rb | 18 + lib/sepa_file_parser/camt052/report.rb | 73 + lib/sepa_file_parser/camt053/base.rb | 18 + lib/sepa_file_parser/camt053/statement.rb | 75 + lib/sepa_file_parser/camt054/base.rb | 18 + lib/sepa_file_parser/camt054/notification.rb | 54 + lib/sepa_file_parser/errors.rb | 12 + lib/sepa_file_parser/file.rb | 10 + lib/sepa_file_parser/general/account.rb | 45 + .../general/account_balance.rb | 61 + lib/sepa_file_parser/general/batch_detail.rb | 24 + lib/sepa_file_parser/general/charges.rb | 25 + lib/sepa_file_parser/general/creditor.rb | 46 + lib/sepa_file_parser/general/debitor.rb | 46 + lib/sepa_file_parser/general/entry.rb | 117 + lib/sepa_file_parser/general/group_header.rb | 38 + .../general/postal_address.rb | 45 + lib/sepa_file_parser/general/record.rb | 49 + lib/sepa_file_parser/general/transaction.rb | 139 ++ lib/sepa_file_parser/general/type/builder.rb | 20 + lib/sepa_file_parser/general/type/code.rb | 13 + .../general/type/proprietary.rb | 13 + lib/sepa_file_parser/misc.rb | 27 + lib/sepa_file_parser/register.rb | 17 + lib/sepa_file_parser/string.rb | 10 + lib/sepa_file_parser/version.rb | 5 + lib/sepa_file_parser/xml.rb | 41 + sepa_file_parser.gemspec | 29 + spec/fixtures/camt052/valid_example.xml | 132 + .../camt052/valid_example_with_dates.xml | 94 + spec/fixtures/camt052/valid_namespace.xml | 4 + spec/fixtures/camt053/valid_example.xml | 368 +++ spec/fixtures/camt053/valid_example_v4.xml | 2136 +++++++++++++++++ spec/fixtures/camt053/valid_example_v8.xml | 372 +++ .../camt053/valid_example_with_batch.xml | 261 ++ .../camt053/valid_example_with_datetime.xml | 112 + .../camt053/valid_example_with_debit.xml | 368 +++ .../camt053/valid_example_with_instdamt.xml | 138 ++ .../camt053/valid_example_with_other_id.xml | 40 + spec/fixtures/camt053/valid_namespace.xml | 4 + spec/fixtures/camt054/valid_example.xml | 437 ++++ spec/fixtures/general/invalid_namespace.xml | 4 + .../lib/sepa_file_parser/camt052/base_spec.rb | 21 + .../sepa_file_parser/camt052/report_spec.rb | 35 + .../lib/sepa_file_parser/camt053/base_spec.rb | 38 + .../camt053/statement_spec.rb | 62 + .../lib/sepa_file_parser/camt054/base_spec.rb | 20 + .../camt054/notification_spec.rb | 19 + spec/lib/sepa_file_parser/file_spec.rb | 23 + .../general/account_balance_spec.rb | 43 + .../sepa_file_parser/general/account_spec.rb | 34 + .../general/batch_detail_spec.rb | 17 + .../sepa_file_parser/general/charges_spec.rb | 13 + .../sepa_file_parser/general/creditor_spec.rb | 35 + .../sepa_file_parser/general/debitor_spec.rb | 34 + .../sepa_file_parser/general/entry_spec.rb | 41 + .../general/group_header_spec.rb | 25 + .../general/postal_address_spec.rb | 41 + .../sepa_file_parser/general/record_spec.rb | 28 + .../general/transaction_spec.rb | 120 + spec/lib/sepa_file_parser/misc_spec.rb | 27 + spec/lib/sepa_file_parser/string_spec.rb | 23 + spec/lib/sepa_file_parser/xml_spec.rb | 82 + spec/spec_helper.rb | 22 + 75 files changed, 6592 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 .rspec create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 LICENSE create mode 100644 READMDE.md create mode 100644 Rakefile create mode 100644 lib/sepa_file_parser.rb create mode 100644 lib/sepa_file_parser/camt052/base.rb create mode 100644 lib/sepa_file_parser/camt052/report.rb create mode 100644 lib/sepa_file_parser/camt053/base.rb create mode 100644 lib/sepa_file_parser/camt053/statement.rb create mode 100644 lib/sepa_file_parser/camt054/base.rb create mode 100644 lib/sepa_file_parser/camt054/notification.rb create mode 100644 lib/sepa_file_parser/errors.rb create mode 100644 lib/sepa_file_parser/file.rb create mode 100644 lib/sepa_file_parser/general/account.rb create mode 100644 lib/sepa_file_parser/general/account_balance.rb create mode 100644 lib/sepa_file_parser/general/batch_detail.rb create mode 100644 lib/sepa_file_parser/general/charges.rb create mode 100644 lib/sepa_file_parser/general/creditor.rb create mode 100644 lib/sepa_file_parser/general/debitor.rb create mode 100644 lib/sepa_file_parser/general/entry.rb create mode 100644 lib/sepa_file_parser/general/group_header.rb create mode 100644 lib/sepa_file_parser/general/postal_address.rb create mode 100644 lib/sepa_file_parser/general/record.rb create mode 100644 lib/sepa_file_parser/general/transaction.rb create mode 100644 lib/sepa_file_parser/general/type/builder.rb create mode 100644 lib/sepa_file_parser/general/type/code.rb create mode 100644 lib/sepa_file_parser/general/type/proprietary.rb create mode 100644 lib/sepa_file_parser/misc.rb create mode 100644 lib/sepa_file_parser/register.rb create mode 100644 lib/sepa_file_parser/string.rb create mode 100644 lib/sepa_file_parser/version.rb create mode 100644 lib/sepa_file_parser/xml.rb create mode 100644 sepa_file_parser.gemspec create mode 100644 spec/fixtures/camt052/valid_example.xml create mode 100644 spec/fixtures/camt052/valid_example_with_dates.xml create mode 100644 spec/fixtures/camt052/valid_namespace.xml create mode 100755 spec/fixtures/camt053/valid_example.xml create mode 100644 spec/fixtures/camt053/valid_example_v4.xml create mode 100644 spec/fixtures/camt053/valid_example_v8.xml create mode 100644 spec/fixtures/camt053/valid_example_with_batch.xml create mode 100644 spec/fixtures/camt053/valid_example_with_datetime.xml create mode 100755 spec/fixtures/camt053/valid_example_with_debit.xml create mode 100755 spec/fixtures/camt053/valid_example_with_instdamt.xml create mode 100755 spec/fixtures/camt053/valid_example_with_other_id.xml create mode 100644 spec/fixtures/camt053/valid_namespace.xml create mode 100644 spec/fixtures/camt054/valid_example.xml create mode 100644 spec/fixtures/general/invalid_namespace.xml create mode 100644 spec/lib/sepa_file_parser/camt052/base_spec.rb create mode 100644 spec/lib/sepa_file_parser/camt052/report_spec.rb create mode 100644 spec/lib/sepa_file_parser/camt053/base_spec.rb create mode 100644 spec/lib/sepa_file_parser/camt053/statement_spec.rb create mode 100644 spec/lib/sepa_file_parser/camt054/base_spec.rb create mode 100644 spec/lib/sepa_file_parser/camt054/notification_spec.rb create mode 100644 spec/lib/sepa_file_parser/file_spec.rb create mode 100644 spec/lib/sepa_file_parser/general/account_balance_spec.rb create mode 100644 spec/lib/sepa_file_parser/general/account_spec.rb create mode 100644 spec/lib/sepa_file_parser/general/batch_detail_spec.rb create mode 100644 spec/lib/sepa_file_parser/general/charges_spec.rb create mode 100644 spec/lib/sepa_file_parser/general/creditor_spec.rb create mode 100644 spec/lib/sepa_file_parser/general/debitor_spec.rb create mode 100644 spec/lib/sepa_file_parser/general/entry_spec.rb create mode 100644 spec/lib/sepa_file_parser/general/group_header_spec.rb create mode 100644 spec/lib/sepa_file_parser/general/postal_address_spec.rb create mode 100644 spec/lib/sepa_file_parser/general/record_spec.rb create mode 100644 spec/lib/sepa_file_parser/general/transaction_spec.rb create mode 100644 spec/lib/sepa_file_parser/misc_spec.rb create mode 100644 spec/lib/sepa_file_parser/string_spec.rb create mode 100644 spec/lib/sepa_file_parser/xml_spec.rb create mode 100644 spec/spec_helper.rb diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e60d3dd --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +.git +build +log diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..5ace460 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..154e64c --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,30 @@ +name: Ruby CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + test: + + runs-on: ubuntu-latest + + strategy: + matrix: + ruby-version: + - 3.2 + - 3.1 + - "3.0" + - 2.7 + + steps: + - uses: actions/checkout@v4 + - name: Set up Ruby ${{ matrix.ruby-version }} + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby-version }} + bundler-cache: true # runs 'bundle install' and caches installed gems automatically + - name: Run tests + run: bundle exec rspec diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa90f5f --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +.DS_Store +.ruby-version +*.gem +*.rbc +.bundle +.config +.yardoc +InstalledFiles +_yardoc +coverage +doc/ +lib/bundler/man +pkg +rdoc +spec/reports +test/tmp +test/version_tmp +tmp diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..83e16f8 --- /dev/null +++ b/.rspec @@ -0,0 +1,2 @@ +--color +--require spec_helper diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..8391f41 --- /dev/null +++ b/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' + +# Specify your gem's dependencies in the .gemspec file +gemspec diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..3288f10 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,48 @@ +PATH + remote: . + specs: + sepa_file_parser (0.1.0) + bigdecimal + nokogiri + time + +GEM + remote: https://rubygems.org/ + specs: + bigdecimal (3.1.8) + builder (3.2.4) + date (3.3.4) + diff-lcs (1.5.1) + mini_portile2 (2.8.6) + nokogiri (1.16.5) + mini_portile2 (~> 2.8.2) + racc (~> 1.4) + racc (1.7.3) + rake (13.2.1) + rspec (3.13.0) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.0) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.1) + time (0.3.0) + date + +PLATFORMS + ruby + +DEPENDENCIES + builder (~> 3.2.4) + rake (~> 13.2.1) + rspec (~> 3.13.0) + sepa_file_parser! + +BUNDLED WITH + 2.4.2 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9f48868 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2024 viafintech GmbH + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/READMDE.md b/READMDE.md new file mode 100644 index 0000000..f6c2f94 --- /dev/null +++ b/READMDE.md @@ -0,0 +1,55 @@ +# SepaFileParser + +[![Ruby CI](https://github.com/viafintech/sepa_file_parser/actions/workflows/test.yml/badge.svg)](https://github.com/viafintech/sepa_file_parser/actions/workflows/test.yml) + +SepaFileParser is a Ruby Gem which does some basic parsing of sepa files, such as camt052, camt053, and camt054 files into an object +structure for easier usability instead of having to use an XML parser all the time. +Keep in mind that this might not include a complete parsing of camt specification. +Fields that we did not need for our use-cases are simply ignored for now. + +## Getting started + +1. add the Gem to the Gemfile + +```ruby +gem 'sepa_file_parser' +``` + +2. Require the Gem at any point before using it +3. Use it! + +## Example for camt053 +```ruby +camt = SepaFileParser::File.parse path_to_file +puts camt.group_header.creation_date_time +camt.statements.each do |statement| + puts statement.account.iban + statement.entries.each do |entry| + # Access individual entries/bank transfers + puts entry.amount + entry.transactions.each do |transaction| + puts transaction.debitor + end + end +end +``` + +Please check the code for fields not mentioned here. +Also check the code for camt052 and camt054. + +## Registering new namespaces +In case you have to parse a namespace which is generally compatible with any of the camt parsers, it is possible to register additional namespaces, without requiring a change to this gem. +```ruby +# Registering a new camt052 namespace +SepaFileParser::Xml.register('', :camt052) +# Registering a new camt053 namespace +SepaFileParser::Xml.register('', :camt053) +# Registering a new camt054 namespace +SepaFileParser::Xml.register('', :camt054) +``` + +## Bugs and Contribution +For bugs and feature requests open an issue on Github. For code contributions fork the repo, make your changes and create a pull request. + +### License +[LICENSE](LICENSE) diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..0823600 --- /dev/null +++ b/Rakefile @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'bundler/gem_tasks' + +task :c do + require 'irb' + require 'irb/completion' + require './lib/sepa_file_parser.rb' + ARGV.clear + IRB.start +end diff --git a/lib/sepa_file_parser.rb b/lib/sepa_file_parser.rb new file mode 100644 index 0000000..fd9e2cc --- /dev/null +++ b/lib/sepa_file_parser.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'nokogiri' +require 'time' +require 'bigdecimal' + +require_relative 'sepa_file_parser/misc' +require_relative 'sepa_file_parser/version' +require_relative 'sepa_file_parser/errors' +require_relative 'sepa_file_parser/general/account_balance' +require_relative 'sepa_file_parser/general/creditor' +require_relative 'sepa_file_parser/general/debitor' +require_relative 'sepa_file_parser/general/entry' +require_relative 'sepa_file_parser/general/record' +require_relative 'sepa_file_parser/general/charges' +require_relative 'sepa_file_parser/general/account' +require_relative 'sepa_file_parser/general/batch_detail' +require_relative 'sepa_file_parser/general/group_header' +require_relative 'sepa_file_parser/general/postal_address' +require_relative 'sepa_file_parser/general/transaction' +require_relative 'sepa_file_parser/general/type/builder' +require_relative 'sepa_file_parser/general/type/code' +require_relative 'sepa_file_parser/general/type/proprietary' +require_relative 'sepa_file_parser/052/report' +require_relative 'sepa_file_parser/052/base' +require_relative 'sepa_file_parser/053/statement' +require_relative 'sepa_file_parser/053/base' +require_relative 'sepa_file_parser/054/notification' +require_relative 'sepa_file_parser/054/base' +require_relative 'sepa_file_parser/file' +require_relative 'sepa_file_parser/string' +require_relative 'sepa_file_parser/register' +require_relative 'sepa_file_parser/xml' diff --git a/lib/sepa_file_parser/camt052/base.rb b/lib/sepa_file_parser/camt052/base.rb new file mode 100644 index 0000000..ec4c9ee --- /dev/null +++ b/lib/sepa_file_parser/camt052/base.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module SepaFileParser + module Camt052 + class Base + attr_reader :group_header, :reports, :xml_data + + def initialize(xml_data) + @xml_data = xml_data + # BkToCstmrAccptRpt = Bank to Customer Account Report + grphdr = xml_data.xpath('BkToCstmrAcctRpt/GrpHdr') + @group_header = CamtParser::GroupHeader.new(grphdr) + reports = xml_data.xpath('BkToCstmrAcctRpt/Rpt') + @reports = reports.map{ |x| Report.new(x) } + end + end + end +end diff --git a/lib/sepa_file_parser/camt052/report.rb b/lib/sepa_file_parser/camt052/report.rb new file mode 100644 index 0000000..47b0491 --- /dev/null +++ b/lib/sepa_file_parser/camt052/report.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +module SepaFileParser + module Camt052 + class Report + + attr_reader :xml_data + + def initialize(xml_data) + @xml_data = xml_data + end + + def identification + @identification ||= xml_data.xpath('Id/text()').text + end + + def generation_date + @generation_date ||= Time.parse(xml_data.xpath('CreDtTm/text()').text) + end + + def account + @account ||= CamtParser::Account.new(xml_data.xpath('Acct').first) + end + + def entries + @entries ||= xml_data.xpath('Ntry').map{ |x| CamtParser::Entry.new(x) } + end + + def legal_sequence_number + @legal_sequence_number ||= xml_data.xpath('LglSeqNb/text()').text + end + + def from_date_time + @from_date_time ||= (x = xml_data.xpath('FrToDt/FrDtTm')).empty? ? nil : Time.parse(x.first.content) + end + + def to_date_time + @to_date_time ||= (x = xml_data.xpath('FrToDt/ToDtTm')).empty? ? nil : Time.parse(x.first.content) + end + + def opening_balance + @opening_balance ||= begin + bal = xml_data.xpath('Bal/Tp//Cd[contains(text(), "PRCD")]').first.ancestors('Bal') + date = bal.xpath('Dt/Dt/text()').text + credit = bal.xpath('CdtDbtInd/text()').text == 'CRDT' + currency = bal.xpath('Amt').attribute('Ccy').value + CamtParser::AccountBalance.new bal.xpath('Amt/text()').text, currency, date, credit + end + end + alias_method :opening_or_intermediary_balance, :opening_balance + + def closing_balance + @closing_balance ||= begin + bal = xml_data.xpath('Bal/Tp//Cd[contains(text(), "CLBD")]').first.ancestors('Bal') + date = bal.xpath('Dt/Dt/text()').text + credit = bal.xpath('CdtDbtInd/text()').text == 'CRDT' + currency = bal.xpath('Amt').attribute('Ccy').value + CamtParser::AccountBalance.new bal.xpath('Amt/text()').text, currency, date, credit + end + end + alias_method :closing_or_intermediary_balance, :closing_balance + + + def source + xml_data.to_s + end + + def self.parse(xml) + self.new Nokogiri::XML(xml).xpath('Report') + end + end + end +end diff --git a/lib/sepa_file_parser/camt053/base.rb b/lib/sepa_file_parser/camt053/base.rb new file mode 100644 index 0000000..09f626c --- /dev/null +++ b/lib/sepa_file_parser/camt053/base.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module SepaFileParser + module Camt053 + class Base + attr_reader :group_header, :statements, :xml_data + + def initialize(xml_data) + @xml_data = xml_data + + grphdr = xml_data.xpath('BkToCstmrStmt/GrpHdr') + @group_header = GroupHeader.new(grphdr) + statements = xml_data.xpath('BkToCstmrStmt/Stmt') + @statements = statements.map{ |x| Statement.new(x) } + end + end + end +end diff --git a/lib/sepa_file_parser/camt053/statement.rb b/lib/sepa_file_parser/camt053/statement.rb new file mode 100644 index 0000000..1957b89 --- /dev/null +++ b/lib/sepa_file_parser/camt053/statement.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +module SepaFileParser + module Camt053 + class Statement + + attr_reader :xml_data + + def initialize(xml_data) + @xml_data = xml_data + end + + def identification + @identification ||= xml_data.xpath('Id/text()').text + end + + def generation_date + @generation_date ||= Time.parse(xml_data.xpath('CreDtTm/text()').text) + end + + def from_date_time + @from_date_time ||= (x = xml_data.xpath('FrToDt/FrDtTm')).empty? ? nil : Time.parse(x.first.content) + end + + def to_date_time + @to_date_time ||= (x = xml_data.xpath('FrToDt/ToDtTm')).empty? ? nil : Time.parse(x.first.content) + end + + def account + @account ||= Account.new(xml_data.xpath('Acct').first) + end + + def entries + @entries ||= xml_data.xpath('Ntry').map{ |x| Entry.new(x) } + end + + def legal_sequence_number + @legal_sequence_number ||= xml_data.xpath('LglSeqNb/text()').text + end + + def electronic_sequence_number + @electronic_sequence_number ||= xml_data.xpath('ElctrncSeqNb/text()').text + end + + def opening_balance + @opening_balance ||= begin + bal = xml_data.xpath('Bal/Tp//Cd[contains(text(), "OPBD") or contains(text(), "PRCD")]').first.ancestors('Bal') + date = bal.xpath('Dt/Dt/text()').text + credit = bal.xpath('CdtDbtInd/text()').text == 'CRDT' + currency = bal.xpath('Amt').attribute('Ccy').value + AccountBalance.new bal.xpath('Amt/text()').text, currency, date, credit + end + end + + def closing_balance + @closing_balance ||= begin + bal = xml_data.xpath('Bal/Tp//Cd[contains(text(), "CLBD")]').first.ancestors('Bal') + date = bal.xpath('Dt/Dt/text()').text + credit = bal.xpath('CdtDbtInd/text()').text == 'CRDT' + currency = bal.xpath('Amt').attribute('Ccy').value + AccountBalance.new bal.xpath('Amt/text()').text, currency, date, credit + end + end + alias_method :closing_or_intermediary_balance, :closing_balance + + def source + xml_data.to_s + end + + def self.parse(xml) + self.new Nokogiri::XML(xml).xpath('Stmt') + end + end + end +end diff --git a/lib/sepa_file_parser/camt054/base.rb b/lib/sepa_file_parser/camt054/base.rb new file mode 100644 index 0000000..4fff3ff --- /dev/null +++ b/lib/sepa_file_parser/camt054/base.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module SepaFileParser + module Camt054 + class Base + attr_reader :group_header, :notifications, :xml_data + + def initialize(xml_data) + @xml_data = xml_data + + grphdr = xml_data.xpath('BkToCstmrDbtCdtNtfctn/GrpHdr') + @group_header = GroupHeader.new(grphdr) + notifications = xml_data.xpath('BkToCstmrDbtCdtNtfctn/Ntfctn') + @notifications = notifications.map{ |x| Notification.new(x) } + end + end + end +end diff --git a/lib/sepa_file_parser/camt054/notification.rb b/lib/sepa_file_parser/camt054/notification.rb new file mode 100644 index 0000000..e5cb05a --- /dev/null +++ b/lib/sepa_file_parser/camt054/notification.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module SepaFileParser + module Camt054 + class Notification + + attr_reader :xml_data + + def initialize(xml_data) + @xml_data = xml_data + end + + # @return [String] + def identification + @identification ||= xml_data.xpath('Id/text()').text + end + + # @return [Time] + def generation_date + @generation_date ||= Time.parse(xml_data.xpath('CreDtTm/text()').text) + end + + # @return [Time, nil] + def from_date_time + @from_date_time ||= (x = xml_data.xpath('FrToDt/FrDtTm')).empty? ? nil : Time.parse(x.first.content) + end + + # @return [Time, nil] + def to_date_time + @to_date_time ||= (x = xml_data.xpath('FrToDt/ToDtTm')).empty? ? nil : Time.parse(x.first.content) + end + + # @return [Account] + def account + @account ||= Account.new(xml_data.xpath('Acct').first) + end + + # @return [Array] + def entries + @entries ||= xml_data.xpath('Ntry').map{ |x| Entry.new(x) } + end + + # @return [String] + def source + xml_data.to_s + end + + # @return [CamtParser::Format054::Notification] + def self.parse(xml) + self.new Nokogiri::XML(xml).xpath('Ntfctn') + end + end + end +end diff --git a/lib/sepa_file_parser/errors.rb b/lib/sepa_file_parser/errors.rb new file mode 100644 index 0000000..922b20b --- /dev/null +++ b/lib/sepa_file_parser/errors.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module SepaFileParser + module Errors + class BaseError < StandardError; end + + class NamespaceAlreadyRegistered < BaseError; end + class NotXMLError < BaseError; end + class UnsupportedParserClass < BaseError; end + class UnsupportedNamespaceError < BaseError; end + end +end diff --git a/lib/sepa_file_parser/file.rb b/lib/sepa_file_parser/file.rb new file mode 100644 index 0000000..ee73939 --- /dev/null +++ b/lib/sepa_file_parser/file.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module SepaFileParser + class File + def self.parse(path) + data = ::File.read(path) + SepaFileParser::String.parse(data) + end + end +end diff --git a/lib/sepa_file_parser/general/account.rb b/lib/sepa_file_parser/general/account.rb new file mode 100644 index 0000000..4af1da0 --- /dev/null +++ b/lib/sepa_file_parser/general/account.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module SepaFileParser + class Account + + attr_reader :xml_data + + def initialize(xml_data) + @xml_data = xml_data + end + + # @return [String] + def iban + @iban ||= xml_data.xpath('Id/IBAN/text()').text + end + + # @return [String] + def other_id + @other_id ||= xml_data.xpath('Id/Othr/Id/text()').text + end + + # @return [String] + def account_number + !iban.nil? && !iban.empty? ? iban : other_id + end + + # @return [String] + def bic + @bic ||= [ + xml_data.xpath('Svcr/FinInstnId/BIC/text()').text, + xml_data.xpath('Svcr/FinInstnId/BICFI/text()').text, + ].reject(&:empty?).first.to_s + end + + # @return [String] + def bank_name + @bank_name ||= xml_data.xpath('Svcr/FinInstnId/Nm/text()').text + end + + # @return [String] + def currency + @currency ||= xml_data.xpath('Ccy/text()').text + end + end +end diff --git a/lib/sepa_file_parser/general/account_balance.rb b/lib/sepa_file_parser/general/account_balance.rb new file mode 100644 index 0000000..c8c07b6 --- /dev/null +++ b/lib/sepa_file_parser/general/account_balance.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +module SepaFileParser + class AccountBalance + + # @param amount [String] + # @param currency [String] + # @param date [String] + # @param credit [Boolean] + def initialize(amount, currency, date, credit = false) + @amount = amount + @currency = currency + @date = date + @credit = credit + end + + # @return [String] + def currency + @currency + end + + # @return [Date] + def date + Date.parse @date + end + + # @return [Integer] either 1 or -1 + def sign + credit? ? 1 : -1 + end + + # @return [Boolean] + def credit? + @credit + end + + # @return [BigDecimal] + def amount + SepaFileParser::Misc.to_amount(@amount) + end + + # @return [Integer] + def amount_in_cents + SepaFileParser::Misc.to_amount_in_cents(@amount) + end + + # @return [BigDecimal] + def signed_amount + amount * sign + end + + # @return [Hash{String => BigDecimal, Integer}] + def to_h + { + 'amount' => amount, + 'amount_in_cents' => amount_in_cents, + 'sign' => sign + } + end + end +end diff --git a/lib/sepa_file_parser/general/batch_detail.rb b/lib/sepa_file_parser/general/batch_detail.rb new file mode 100644 index 0000000..866c644 --- /dev/null +++ b/lib/sepa_file_parser/general/batch_detail.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module SepaFileParser + class BatchDetail + + attr_reader :xml_data + + def initialize(xml_data) + @xml_data = xml_data + end + + def payment_information_identification + @payment_information_identification ||= xml_data.xpath('PmtInfId/text()').text + end + + def msg_id # may be missing + @msg_id ||= xml_data.xpath('MsgId/text()').text + end + + def number_of_transactions + @number_of_transactions ||= xml_data.xpath('NbOfTxs/text()').text + end + end +end diff --git a/lib/sepa_file_parser/general/charges.rb b/lib/sepa_file_parser/general/charges.rb new file mode 100644 index 0000000..d8531e9 --- /dev/null +++ b/lib/sepa_file_parser/general/charges.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module SepaFileParser + class Charges + + attr_reader :xml_data + + def initialize(xml_data) + @xml_data = xml_data + @total_charges_and_tax_amount = xml_data.xpath('TtlChrgsAndTaxAmt/text()').text + end + + def total_charges_and_tax_amount + SepaFileParser::Misc.to_amount(@total_charges_and_tax_amount) + end + + def total_charges_and_tax_amount_in_cents + SepaFileParser::Misc.to_amount_in_cents(@total_charges_and_tax_amount) + end + + def records + @records ||= xml_data.xpath('Rcrd').map{ |x| SepaFileParser::Record.new(x) } + end + end +end diff --git a/lib/sepa_file_parser/general/creditor.rb b/lib/sepa_file_parser/general/creditor.rb new file mode 100644 index 0000000..a54e8bc --- /dev/null +++ b/lib/sepa_file_parser/general/creditor.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module SepaFileParser + class Creditor + + attr_reader :xml_data + + def initialize(xml_data) + @xml_data = xml_data + end + + def name + @name ||= [ + xml_data.xpath('RltdPties/Cdtr/Nm/text()').text, + xml_data.xpath('RltdPties/Cdtr/Pty/Nm/text()').text, + ].reject(&:empty?).first.to_s + end + + def iban + @iban ||= xml_data.xpath('RltdPties/CdtrAcct/Id/IBAN/text()').text + end + + def bic + @bic ||= [ + xml_data.xpath('RltdAgts/CdtrAgt/FinInstnId/BIC/text()').text, + xml_data.xpath('RltdAgts/CdtrAgt/FinInstnId/BICFI/text()').text, + ].reject(&:empty?).first.to_s + end + + def bank_name + @bank_name ||= xml_data.xpath('RltdAgts/CdtrAgt/FinInstnId/Nm/text()').text + end + + # @return [SepaFileParser::PostalAddress, nil] + def postal_address # May be missing + postal_address = [ + xml_data.xpath('RltdPties/Cdtr/PstlAdr'), + xml_data.xpath('RltdPties/Cdtr/Pty/PstlAdr'), + ].reject(&:empty?).first + + return nil if postal_address == nil || postal_address.empty? + + @address ||= SepaFileParser::PostalAddress.new(postal_address) + end + end +end diff --git a/lib/sepa_file_parser/general/debitor.rb b/lib/sepa_file_parser/general/debitor.rb new file mode 100644 index 0000000..b29a36a --- /dev/null +++ b/lib/sepa_file_parser/general/debitor.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module SepaFileParser + class Debitor + + attr_reader :xml_data + + def initialize(xml_data) + @xml_data = xml_data + end + + def name + @name ||= [ + xml_data.xpath('RltdPties/Dbtr/Nm/text()').text, + xml_data.xpath('RltdPties/Dbtr/Pty/Nm/text()').text, + ].reject(&:empty?).first.to_s + end + + def iban + @iban ||= xml_data.xpath('RltdPties/DbtrAcct/Id/IBAN/text()').text + end + + def bic + @bic ||= [ + xml_data.xpath('RltdAgts/DbtrAgt/FinInstnId/BIC/text()').text, + xml_data.xpath('RltdAgts/DbtrAgt/FinInstnId/BICFI/text()').text, + ].reject(&:empty?).first.to_s + end + + def bank_name + @bank_name ||= xml_data.xpath('RltdAgts/DbtrAgt/FinInstnId/Nm/text()').text + end + + # @return [SepaFileParser::PostalAddress, nil] + def postal_address # May be missing + postal_address = [ + xml_data.xpath('RltdPties/Dbtr/PstlAdr'), + xml_data.xpath('RltdPties/Dbtr/Pty/PstlAdr'), + ].reject(&:empty?).first + + return nil if postal_address == nil || postal_address.empty? + + @address ||= SepaFileParser::PostalAddress.new(postal_address) + end + end +end diff --git a/lib/sepa_file_parser/general/entry.rb b/lib/sepa_file_parser/general/entry.rb new file mode 100644 index 0000000..6ae1717 --- /dev/null +++ b/lib/sepa_file_parser/general/entry.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +module SepaFileParser + class Entry + + attr_reader :xml_data + + def initialize(xml_data) + @xml_data = xml_data + @amount = xml_data.xpath('Amt/text()').text + end + + def amount + SepaFileParser::Misc.to_amount(@amount) + end + + def amount_in_cents + SepaFileParser::Misc.to_amount_in_cents(@amount) + end + + # @return [String] + def currency + @currency ||= xml_data.xpath('Amt/@Ccy').text + end + + # @return [Boolean] + def debit + @debit ||= xml_data.xpath('CdtDbtInd/text()').text.upcase == 'DBIT' + end + + # @return [Date] + def value_date + @value_date ||= ((date = xml_data.xpath('ValDt/Dt/text()').text).empty? ? nil : Date.parse(date)) + end + + # @return [Date] + def booking_date + @booking_date ||= ((date = xml_data.xpath('BookgDt/Dt/text()').text).empty? ? nil : Date.parse(date)) + end + + # @return [DateTime] + def value_datetime + @value_datetime ||= ((datetime = xml_data.xpath('ValDt/DtTm/text()').text).empty? ? nil : DateTime.parse(datetime)) + end + + # @return [DateTime] + def booking_datetime + @booking_datetime ||= ((datetime = xml_data.xpath('BookgDt/DtTm/text()').text).empty? ? nil : DateTime.parse(datetime)) + end + + # @return [String] + def bank_reference # May be missing + @bank_reference ||= xml_data.xpath('AcctSvcrRef/text()').text + end + + # @return [Array] + def transactions + @transactions ||= parse_transactions + end + + # @return [Boolean] + def credit? + !debit + end + + # @return [Boolean] + def debit? + debit + end + + # @return [Integer] either 1 or -1 + def sign + credit? ? 1 : -1 + end + + # @return [Boolean] + def reversal? + @reversal ||= xml_data.xpath('RvslInd/text()').text.downcase == 'true' + end + + # @return [Boolean] + def booked? + @booked ||= xml_data.xpath('Sts/text()').text.upcase == 'BOOK' + end + + # @return [String] + def additional_information + @additional_information ||= xml_data.xpath('AddtlNtryInf/text()').text + end + alias_method :description, :additional_information + + # @return [SepaFileParser::Charges] + def charges + @charges ||= SepaFileParser::Charges.new(xml_data.xpath('Chrgs')) + end + # @return [SepaFileParser::BatchDetail, nil] + def batch_detail + @batch_detail ||= xml_data.xpath('NtryDtls/Btch').empty? ? nil : SepaFileParser::BatchDetail.new(@xml_data.xpath('NtryDtls/Btch')) + end + + private + + def parse_transactions + transaction_details = xml_data.xpath('NtryDtls/TxDtls') + + amt = nil + ccy = nil + + if transaction_details.length == 1 + amt = xml_data.xpath('Amt/text()').text + ccy = xml_data.xpath('Amt/@Ccy').text + end + + xml_data.xpath('NtryDtls/TxDtls').map { |x| Transaction.new(x, debit?, amt, ccy) } + end + end +end diff --git a/lib/sepa_file_parser/general/group_header.rb b/lib/sepa_file_parser/general/group_header.rb new file mode 100644 index 0000000..719c916 --- /dev/null +++ b/lib/sepa_file_parser/general/group_header.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'time' + +module SepaFileParser + class GroupHeader + + attr_reader :message_id, + :creation_date_time, + :additional_information, + :message_pagination, + :xml_data + + def initialize(xml_data) + @xml_data = xml_data + @message_id = xml_data.xpath('MsgId/text()').text + @creation_date_time = Time.parse(xml_data.xpath('CreDtTm/text()').text) + @message_pagination = (x = xml_data.xpath('MsgPgntn')).empty? ? nil : MessagePagination.new(x) + @additional_information = xml_data.xpath('AddtlInf/text()').text + end + end + + class MessagePagination + + attr_reader :page_number, + :xml_data + + def initialize(xml_data) + @xml_data = xml_data + @page_number = xml_data.xpath('PgNb/text()').text.to_i + @last_page_indicator = xml_data.xpath('LastPgInd/text()').text == 'true' + end + + def last_page? + @last_page_indicator + end + end +end diff --git a/lib/sepa_file_parser/general/postal_address.rb b/lib/sepa_file_parser/general/postal_address.rb new file mode 100644 index 0000000..7ce9089 --- /dev/null +++ b/lib/sepa_file_parser/general/postal_address.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module SepaFileParser + class PostalAddress + + attr_reader :xml_data + + def initialize(xml_data) + @xml_data = xml_data + end + + # @return [Array] + def lines # May be empty + xml_data.xpath('AdrLine').map do |x| + x.xpath('text()').text + end + end + + # @return [String] + def street_name # May be missing + xml_data.xpath('StrtNm/text()').text + end + + # @return [String] + def building_number # May be missing + xml_data.xpath('BldgNb/text()').text + end + + # @return [String] + def postal_code # May be missing + xml_data.xpath('PstCd/text()').text + end + + # @return [String] + def town_name # May be missing + xml_data.xpath('TwnNm/text()').text + end + + # @return [String] + def country # May be missing + xml_data.xpath('Ctry/text()').text + end + + end +end diff --git a/lib/sepa_file_parser/general/record.rb b/lib/sepa_file_parser/general/record.rb new file mode 100644 index 0000000..3978e4f --- /dev/null +++ b/lib/sepa_file_parser/general/record.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module SepaFileParser + class Record + + attr_reader :xml_data + + def initialize(xml_data) + @xml_data = xml_data + @amount = xml_data.xpath('Amt/text()').text + end + + def amount + SepaFileParser::Misc.to_amount(@amount) + end + + def amount_in_cents + SepaFileParser::Misc.to_amount_in_cents(@amount) + end + + def currency + @currency ||= xml_data.xpath('Amt/@Ccy').text + end + + def type + @type ||= SepaFileParser::Type::Builder.build_type(xml_data.xpath('Tp')) + end + + def charges_included? + @charges_included ||= xml_data.xpath('ChrgInclInd/text()').text.downcase == 'true' + end + + def debit + @debit ||= xml_data.xpath('CdtDbtInd/text()').text.upcase == 'DBIT' + end + + def credit? + !debit + end + + def debit? + debit + end + + def sign + credit? ? 1 : -1 + end + end +end diff --git a/lib/sepa_file_parser/general/transaction.rb b/lib/sepa_file_parser/general/transaction.rb new file mode 100644 index 0000000..07989c2 --- /dev/null +++ b/lib/sepa_file_parser/general/transaction.rb @@ -0,0 +1,139 @@ +# frozen_string_literal: true + +module SepaFileParser + class Transaction + + attr_reader :xml_data + + def initialize(xml_data, debit, amount = nil, currency = nil) + @xml_data = xml_data + @debit = debit + @amount = parse_amount || amount + @currency = parse_currency || currency + end + + def amount + SepaFileParser::Misc.to_amount(@amount) + end + + def amount_in_cents + SepaFileParser::Misc.to_amount_in_cents(@amount) + end + + def currency + @currency + end + + def creditor + @creditor ||= SepaFileParser::Creditor.new(xml_data) + end + + def debitor + @debitor ||= SepaFileParser::Debitor.new(xml_data) + end + + def name + credit? ? debitor.name : creditor.name + end + + def iban + credit? ? debitor.iban : creditor.iban + end + + def bic + credit? ? debitor.bic : creditor.bic + end + + def postal_address + credit? ? debitor.postal_address : creditor.postal_address + end + + def credit? + !debit + end + + def debit? + debit + end + + def debit + @debit + end + + def sign + credit? ? 1 : -1 + end + + def remittance_information + @remittance_information ||= begin + if (x = xml_data.xpath('RmtInf/Ustrd')).empty? + nil + else + x.collect(&:content).join(' ') + end + end + end + + def swift_code + @swift_code ||= xml_data.xpath('BkTxCd/Prtry/Cd/text()').text.split('+')[0] + end + + def reference + @reference ||= xml_data.xpath('Refs/InstrId/text()').text + end + + def bank_reference # May be missing + @bank_reference ||= xml_data.xpath('Refs/AcctSvcrRef/text()').text + end + + def end_to_end_reference # May be missing + @end_to_end_reference ||= xml_data.xpath('Refs/EndToEndId/text()').text + end + + def mandate_reference # May be missing + @mandate_reference ||= xml_data.xpath('Refs/MndtId/text()').text + end + + def creditor_reference # May be missing + @creditor_reference ||= xml_data.xpath('RmtInf/Strd/CdtrRefInf/Ref/text()').text + end + + def transaction_id # May be missing + @transaction_id ||= xml_data.xpath('Refs/TxId/text()').text + end + + def creditor_identifier # May be missing + @creditor_identifier ||= xml_data.xpath('RltdPties/Cdtr/Id/PrvtId/Othr/Id/text()').text + end + + def payment_information # May be missing + @payment_information ||= xml_data.xpath('Refs/PmtInfId/text()').text + end + + def additional_information # May be missing + @addition_information ||= xml_data.xpath('AddtlTxInf/text()').text + end + + def reason_code # May be missing + @reason_code ||= xml_data.xpath('RtrInf/Rsn/Cd/text()').text + end + + private + + def parse_amount + if xml_data.xpath('Amt').any? + xml_data.xpath('Amt/text()').text + elsif xml_data.xpath('AmtDtls').any? + xml_data.xpath('AmtDtls//Amt/text()').first.text + end + end + + def parse_currency + if xml_data.xpath('Amt').any? + xml_data.xpath('Amt/@Ccy').text + elsif xml_data.xpath('AmtDtls').any? + xml_data.xpath('AmtDtls//Amt/@Ccy').first.text + end + end + end +end diff --git a/lib/sepa_file_parser/general/type/builder.rb b/lib/sepa_file_parser/general/type/builder.rb new file mode 100644 index 0000000..0f1d18f --- /dev/null +++ b/lib/sepa_file_parser/general/type/builder.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module SepaFileParser + module Type + class Builder + def self.build_type(xml_data) + if xml_data.xpath('Prtry').any? + Proprietary.new( + xml_data.xpath('Prtry/Id/text()').text, + xml_data.xpath('Prtry/Issr/text()').text + ) + elsif xml_data.xpath('Cd').any? + Code.new(xml_data.xpath('Cd/text()').text) + else + nil + end + end + end + end +end diff --git a/lib/sepa_file_parser/general/type/code.rb b/lib/sepa_file_parser/general/type/code.rb new file mode 100644 index 0000000..8e2678f --- /dev/null +++ b/lib/sepa_file_parser/general/type/code.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module SepaFileParser + module Type + class Code + attr_reader :code + + def initialize(code) + @code = code + end + end + end +end diff --git a/lib/sepa_file_parser/general/type/proprietary.rb b/lib/sepa_file_parser/general/type/proprietary.rb new file mode 100644 index 0000000..4b48468 --- /dev/null +++ b/lib/sepa_file_parser/general/type/proprietary.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module SepaFileParser + module Type + class Proprietary + attr_reader :id, :issuer + + def initialize(id, issuer) + @id, @issuer = id, issuer + end + end + end +end diff --git a/lib/sepa_file_parser/misc.rb b/lib/sepa_file_parser/misc.rb new file mode 100644 index 0000000..d240f8a --- /dev/null +++ b/lib/sepa_file_parser/misc.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module SepaFileParser + class Misc + class << self + + # @param value [nil, String] + # @return [Integer, nil] + def to_amount_in_cents(value) + return nil if value == nil || value.strip == '' + + # Using dollars and cents as representation for parts before and after the decimal separator + dollars, cents = value.split(/,|\./) + cents ||= '0' + format('%s%s', dollars, cents.ljust(2, '0')).to_i + end + + # @param value [nil, String] + # @return [BigDecimal, nil] + def to_amount(value) + return nil if value == nil || value.strip == '' + + BigDecimal(value.tr(',', '.')) + end + end + end +end diff --git a/lib/sepa_file_parser/register.rb b/lib/sepa_file_parser/register.rb new file mode 100644 index 0000000..53ee2b2 --- /dev/null +++ b/lib/sepa_file_parser/register.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require_relative './xml' + +# Add registrations +## CAMT052 +SepaFileParser::Xml.register('urn:iso:std:iso:20022:tech:xsd:camt.052.001.02', :camt052) + +## CAMT053 +SepaFileParser::Xml.register('urn:iso:std:iso:20022:tech:xsd:camt.053.001.02', :camt053) +SepaFileParser::Xml.register('urn:iso:std:iso:20022:tech:xsd:camt.053.001.04', :camt053) +SepaFileParser::Xml.register('urn:iso:std:iso:20022:tech:xsd:camt.053.001.08', :camt053) +SepaFileParser::Xml.register('urn:iso:std:iso:20022:tech:xsd:camt.053.001.10', :camt053) + +## CAMT054 +SepaFileParser::Xml.register('urn:iso:std:iso:20022:tech:xsd:camt.054.001.02', :camt054) +SepaFileParser::Xml.register('urn:iso:std:iso:20022:tech:xsd:camt.054.001.04', :camt054) diff --git a/lib/sepa_file_parser/string.rb b/lib/sepa_file_parser/string.rb new file mode 100644 index 0000000..96111e3 --- /dev/null +++ b/lib/sepa_file_parser/string.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module SepaFileParser + class String + def self.parse(raw_camt) + doc = Nokogiri::XML raw_camt + SepaFileParser::Xml.parse(doc) + end + end +end diff --git a/lib/sepa_file_parser/version.rb b/lib/sepa_file_parser/version.rb new file mode 100644 index 0000000..add27af --- /dev/null +++ b/lib/sepa_file_parser/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module SepaFileParser + VERSION = '0.1.0'.freeze +end diff --git a/lib/sepa_file_parser/xml.rb b/lib/sepa_file_parser/xml.rb new file mode 100644 index 0000000..bc48a1b --- /dev/null +++ b/lib/sepa_file_parser/xml.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module SepaFileParser + class Xml + PARSER_MAPPING = { + camt052: SepaFileParser::Camt052::Base, + camt053: SepaFileParser::Camt053::Base, + camt054: SepaFileParser::Camt054::Base + }.freeze + + SUPPORTED_PARSERS = PARSER_MAPPING.keys.freeze + + @namespace_parsers = {} + + def self.register(namespace, parser) + if !SUPPORTED_PARSERS.include?(parser) + raise SepaFileParser::Errors::UnsupportedParserClass, parser.to_s + end + + if @namespace_parsers.key?(namespace) # Prevent overwriting existing registrations + raise SepaFileParser::Errors::NamespaceAlreadyRegistered, namespace + end + + @namespace_parsers[namespace] = PARSER_MAPPING[parser] + end + + def self.parse(doc) + raise SepaFileParser::Errors::NotXMLError, doc.class unless doc.is_a? Nokogiri::XML::Document + + namespace = doc.namespaces['xmlns'] + doc.remove_namespaces! + + parser_class = @namespace_parsers[namespace] + if parser_class == nil + raise SepaFileParser::Errors::UnsupportedNamespaceError, namespace + end + + return parser_class.new(doc.xpath('Document')) + end + end +end diff --git a/sepa_file_parser.gemspec b/sepa_file_parser.gemspec new file mode 100644 index 0000000..ab1f14c --- /dev/null +++ b/sepa_file_parser.gemspec @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +lib = File.expand_path('../lib', __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'sepa_file_parser/version' + +Gem::Specification.new do |spec| + # For explanations see http://docs.rubygems.org/read/chapter/20 + spec.name = 'sepa_file_parser' + spec.version = SepaFileParser::VERSION + spec.authors = ['Tobias Schoknecht'] + spec.email = ['tobias.schoknecht@viafintech.com'] + spec.description = %q{A parser for sepa files format} + spec.summary = %q{Gem for parsing camt, pain, ... files into a speaking object.} + spec.homepage = 'https://github.com/viafintech/sepa_file_parser' + spec.license = 'MIT' + + spec.files = Dir['lib/**/*.rb', 'lib/**/*.rake'] # Important! + spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) + spec.require_paths = ['lib'] + + spec.add_development_dependency 'rake', '~> 13.2.1' + spec.add_development_dependency 'rspec', '~> 3.13.0' + spec.add_development_dependency 'builder', '~> 3.2.4' + + spec.add_runtime_dependency 'bigdecimal' + spec.add_runtime_dependency 'nokogiri' + spec.add_runtime_dependency 'time' +end diff --git a/spec/fixtures/camt052/valid_example.xml b/spec/fixtures/camt052/valid_example.xml new file mode 100644 index 0000000..e25c57b --- /dev/null +++ b/spec/fixtures/camt052/valid_example.xml @@ -0,0 +1,132 @@ + + + + + + 052D2013-12-27T11:02:03.0N130000005 + 2013-12-27T11:01:46.0+01:00 + + 1 + true + + + + 0352C5220131227110203 + 130000005 + 2013-12-27T11:01:46.0+01:00 + + + DE58740618130100033626 + + EUR + + Testkonto Nummer 2 + + + + GENODEF1PFK + VR-Bank Rottal-Inn eG + + DE 129267947 + UmsStId + + + + + + + + PRCD + + + 8.50 + CRDT +
+
2013-12-27
+ +
+ + + + CLBD + + + 13.50 + CRDT +
+
2013-12-27
+ +
+ + 2.00 + CRDT + BOOK + +
2013-12-27
+
+ +
2013-12-27
+
+ 2013122710583450000 + + + + + + NMSC+051 + ZKA + + + + + Testkonto Nummer 1 + + + + + 740618130000033626 + + BBAN + + + + + + + TEST BERWEISUNG MITTELS BLZ UND KONTONUMMER - DTA + + + +
+ + 3.00 + CRDT + BOOK + +
2013-12-27
+
+ +
2013-12-27
+
+ 2013122710583670000 + + + + + NOTPROVIDED + + + + NMSC+051 + ZKA + + + + Testüberweisung mit BIC und IBAN SEPA IBAN: DE14740618130000033626 BIC: GENODEF1PFK + + + +
+
+
+
diff --git a/spec/fixtures/camt052/valid_example_with_dates.xml b/spec/fixtures/camt052/valid_example_with_dates.xml new file mode 100644 index 0000000..d31c58c --- /dev/null +++ b/spec/fixtures/camt052/valid_example_with_dates.xml @@ -0,0 +1,94 @@ + + + + + camt.052-2019-08-09-11-17-11 + 2019-08-09T11:17:11:735 + + Test User + + Test street + 1 + Test town + 11111 + GB + + + + + 1234567890-2019-08-09-11-17-11 + 2019-08-09T11:17:11:735 + + 2013-01-01T00:00:00:000 + 2019-08-09T00:00:00:000 + + + + + 0000000000000001 + + + EUR + + + TESTBKGB + Test bank + + 1234 + + + + + + 5.00 + DBIT + BOOK + +
2019-07-19
+
+ + + 00000000000 + Test Banking Association + + + + + + 0000000AA000000 + + 5.00 + DBIT + + + + + 0000000000000000 + + + CARD TRANSACTIONS EUR + + + PAYPAL - TEST/000000 + + LUX + + + + + + + + 1234 + + + + + + + + +
+
+
+
diff --git a/spec/fixtures/camt052/valid_namespace.xml b/spec/fixtures/camt052/valid_namespace.xml new file mode 100644 index 0000000..8475c1c --- /dev/null +++ b/spec/fixtures/camt052/valid_namespace.xml @@ -0,0 +1,4 @@ + + + + diff --git a/spec/fixtures/camt053/valid_example.xml b/spec/fixtures/camt053/valid_example.xml new file mode 100755 index 0000000..4080cc4 --- /dev/null +++ b/spec/fixtures/camt053/valid_example.xml @@ -0,0 +1,368 @@ + + + + + + 053D2013-12-27T22:05:03.0N130000005 + 2013-12-27T22:04:52.0+01:00 + + 1 + true + + + + 0352C5320131227220503 + 130000005 + 2013-12-27T22:04:52.0+01:00 + + + DE14740618130000033626 + + 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 + + + + + BankReference + EndToEndReference + MandateReference + PaymentIdentification + UniqueTransactionId + + AdditionalTransactionInformation + + + NTRF+020 + ZKA + + + + + Wayne Enterprises + + + + DE24302201900609832118 + + + + + + + CreditorIdentifier + + + + Testkonto Nummer 2 + + Berlin + Infinite Loop 2 + 12345 + + + + + DE09300606010012345671 + + + CACC + + + + + + + DAAEDEDDXXX + + + ABCDEF + + 1232344234234 + + + + + + DAAEDEDDXXX + + + ABCDEF + + 123456789 + + Bank + + Infinite Loop 1 + Berlin + + + + + + TEST BERWEISUNG MITTELS BLZUND KONTONUMMER - DTA + + + + Überweisungs-Gutschrift; GVC: SEPA Credit Transfer (Einzelbuchung-Haben) +
+ + 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 + + + +
+
+
+
diff --git a/spec/fixtures/camt053/valid_example_v4.xml b/spec/fixtures/camt053/valid_example_v4.xml new file mode 100644 index 0000000..eb103f4 --- /dev/null +++ b/spec/fixtures/camt053/valid_example_v4.xml @@ -0,0 +1,2136 @@ + + + + + 20160430375204000008573 + 2016-04-30T11:50:00 + + 1 + true + + Productive + + + 20160430375204000008574 + 85 + 2016-04-30T11:50:00 + + 2016-04-30T00:00:00 + 2016-04-30T23:59:59 + + + + CH0309000000250090342 + + + Robert Schneider SA Grands magasins Biel/Bienne + + + + + + OPBD + + + 322152.16 + CRDT +
+
2016-04-29
+ +
+ + + + CLBD + + + 322689.77 + CRDT +
+
2016-04-30
+ +
+ + 24.00 + DBIT + false + BOOK + +
2016-04-30
+
+ +
2016-04-30
+
+ 20160401001027080060699001000107 + + + ACMT + + ADOP + CHRG + + + + + + + 20160401001027080060699001000107 + + 24.00 + DBIT + + + UEBRIGE: 25-9034-2 FÜR KONTOAUSZUG PAPIER TÄGLICH +
+ + 328.75 + DBIT + false + BOOK + +
2016-04-30
+
+ +
2016-04-30
+
+ 20160430001001080060699000901107 + + + PMNT + + RCDT + CHRG + + + + + + + 20160430001001080060699000901107 + + 328.75 + DBIT + + + UEBRIGE: 25-9034-2 FÜR EINZAHLUNGEN AM SCHALTER ES +
+ + 200.80 + DBIT + false + BOOK + +
2016-04-30
+
+ +
2016-04-30
+
+ 20160430001001080060699015801107 + + + PMNT + + RCDT + CHRG + + + + + + + 20160430001001080060699015801107 + + 200.80 + DBIT + + + UEBRIGE: 25-9034-2 FÜR GUTSCHRIFT ES VOLLERFASST +
+ + 638.15 + DBIT + false + BOOK + +
2016-04-30
+
+ +
2016-04-30
+
+ 20160430001025080060699000000012 + + + PMNT + + RCDT + CHRG + + + + + + + 20160430001025080060699000000012 + + 638.15 + DBIT + + + ESR-TEILNEHMERNUMMER: 01-000000-4 FÜR EINZAHLUNGEN AM SCHALTER ESR +
+ + 24.00 + DBIT + false + BOOK + +
2016-04-30
+
+ +
2016-04-30
+
+ 20160430001025080060699000000031 + + + PMNT + + RCDT + CHRG + + + + + + + 20160430001025080060699000000031 + + 24.00 + DBIT + + + ESR-TEILNEHMERNUMMER: 01-000000-4 FÜR REJECTBEHANDLUNG ESR DES VORMONATS +
+ + 35.72 + DBIT + false + BOOK + +
2016-04-30
+
+ +
2016-04-30
+
+ 20160430001025080060699000010012 + + + PMNT + + RCDT + CHRG + + + + + + + 20160430001025080060699000010012 + + 35.72 + DBIT + + + ESR-TEILNEHMERNUMMER: 01-000000-4 FÜR NACHBEARBEITUNG ESR+ +
+ + 010000004 + 1000.00 + CRDT + false + BOOK + +
2016-05-27
+
+ +
2016-05-30
+
+ 20160530007602769939022000000012 + + + PMNT + + RCDT + VCOM + + + + + 5.20 + + 5.40 + DBIT + false + + + 2 + + + + + 0.20 + CRDT + true + + DISC + + + + + + 10 + + + + 160527CH00T3L0GE + + 04 + 20160526531801000100274 + + + 100.00 + CRDT + + + PMNT + + CNTR + CDPT + + + + + 1.24 + + 1.20 + DBIT + false + + + 2 + + + + + 0.04 + DBIT + false + + + 4 + + + + + + + Hans Kaufmann + + + + + + POFICHBEXXX + POSTFINANCE AG + + MINGERSTRASSE 20 + 3030 BERNE + + + + + + + + + + ISR Reference + + + CreditorReference + + ?REJECT?0 + + + + 2016-05-26T20:00:00 + + + + + 160527CH00T3KWWA + + 01 + 160527CH00T3KWWA + + + 50.00 + CRDT + + + PMNT + + RCDT + AUTT + + + + + + Rutschmann Pia + + Marktgasse + 28 + 9400 + Rorschach + CH + + + + + + + POFICHBEXXX + POSTFINANCE AG + + MINGERSTRASSE 20 + 3030 BERNE + + + + + + + + + + ISR Reference + + + 000000000002016030002820804 + + ?REJECT?0 + + + + 2016-05-27T20:00:00 + + + + + 160527CH00T2H28C + + 01 + 160527CH00T2H28C + + + 200.00 + CRDT + + + PMNT + + RCDT + ATXN + + + + + + + + 000048358 + + Credit Suisse AG + + Paradeplatz 8 + 8070 Zürich + + + + + + + + + + ISR Reference + + + 000000000002016030001581632 + + ?REJECT?0 + + + + 2016-05-27T20:00:00 + + + + + 160527CH00T3L0W5 + + 04 + 20160526838805000100263 + + + 50.00 + CRDT + + + PMNT + + CNTR + CDPT + + + + + 0.94 + + 0.90 + DBIT + false + + + 2 + + + + + 0.04 + DBIT + false + + + 4 + + + + + + + + POFICHBEXXX + POSTFINANCE AG + + MINGERSTRASSE 20 + 3030 BERNE + + + + + + + + + + ISR Reference + + + 000000000002015110003015939 + + ?REJECT?0 + + + + 2016-05-26T20:00:00 + + + + + 160527CH00T3L0NC + + 04 + 20160526017105000100040 + + + 100.00 + CRDT + + + PMNT + + CNTR + CDPT + + + + + 1.24 + + 1.20 + DBIT + false + + + 2 + + + + + 0.04 + DBIT + false + + + 4 + + + + + + + + POFICHBEXXX + POSTFINANCE AG + + MINGERSTRASSE 20 + 3030 BERNE + + + + + + + + + + ISR Reference + + + 000000000002016030000535990 + + ?REJECT?0 + + + + 2016-05-26T20:00:00 + + + + + 160527CH00T3KXDV + + 01 + 160527CH00T3KXDV + + + 200.00 + CRDT + + + PMNT + + RCDT + AUTT + + + + + + Bernasconi Maria + + Place de la Gare + 12 + 2502 + Biel/Bienne + CH + + + + + CH4444444444444444444 + + + + + + + POFICHBEXXX + POSTFINANCE AG + + MINGERSTRASSE 20 + 3030 BERNE + + + + + + + + + + ISR Reference + + + 000000000002016030002463591 + + ?REJECT?0 + + + + 2016-05-27T20:00:00 + + + + + 160527CH00T3KZO9 + + 04 + 20160526252501000100033 + + + 50.00 + CRDT + + + PMNT + + CNTR + CDPT + + + + + 0.94 + + 0.90 + DBIT + false + + + 2 + + + + + 0.04 + DBIT + false + + + 4 + + + + + + + + POFICHBEXXX + POSTFINANCE AG + + MINGERSTRASSE 20 + 3030 BERNE + + + + + + + + + + ISR Reference + + + 000000000002016030002780613 + + ?REJECT?0 + + + + 2016-05-26T20:00:00 + + + + + 160527CH00T3L0V2 + + 04 + 20160526832003000100047 + + + 100.00 + CRDT + + + PMNT + + CNTR + CDPT + + + + + 1.24 + + 1.20 + DBIT + false + + + 2 + + + + + 0.04 + DBIT + false + + + 4 + + + + + + + + POFICHBEXXX + POSTFINANCE AG + + MINGERSTRASSE 20 + 3030 BERNE + + + + + + + + + + ISR Reference + + + 000000000002016030001554373 + + ?REJECT?0 + + + + 2016-05-26T20:00:00 + + + + + 160527CH00T99DU7 + + 01 + 160527CH00T99DU7 + + + 50.00 + CRDT + + + PMNT + + RCDT + AUTT + + + + + + Zürcher Kantonalbank + + Bahnhofstrasse + 9 + 8001 + Zürich + CH + + + + + CH0000000000000000000 + + + + + + + POFICHBEXXX + POSTFINANCE AG + + MINGERSTRASSE 20 + 3030 BERNE + + + + + + + + + + ISR Reference + + + 000000000002016030000985620 + + ?REJECT?0 + + + + 2016-05-27T20:00:00 + + + + + 160527CH00T99EBW + + 01 + 160527CH00T99EBW + + + 100.00 + CRDT + + + PMNT + + RCDT + AUTT + + + + + + UBS Switzerland AG + + Bahnhofstrasse + 45 + 8001 + Zürich + CH + + + + + CH0000000000000000000 + + + + + + + POFICHBEXXX + POSTFINANCE AG + + MINGERSTRASSE 20 + 3030 BERNE + + + + + + + + + + ISR Reference + + + 000000000002016030001593614 + + ?REJECT?0 + + + + 2016-05-27T20:00:00 + + + + SAMMELGUTSCHRIFT ESR VERARBEITUNG VOM 27.05.2016 KUNDENNUMMER 01-000000-4 PAKET ID: 160527CH000000RU +
+ + 620.00 + CRDT + false + BOOK + +
2016-05-27
+
+ +
2016-05-27
+
+ 20160527007602769947016000000012 + + + PMNT + + RCDT + AUTT + + + + + 7.20 + + 4.80 + DBIT + false + + + 2 + + + + + 2.40 + DBIT + false + + + 5 + + + + + + + 8 + + + + 160527CH00T2UENT + + 00 + 20160527375204000060262 + + + 50.00 + CRDT + + + PMNT + + RCDT + ATXN + + + + + + Rutschmann Pia + + Marktgasse 28 + CH/9400 Rorschach + + + + + CH1111111111111111111 + + + + + + + + 000048358 + + Credit Suisse AG + + Paradeplatz 8 + 8070 Zürich + + + + + + MUSTER MITTEILUNG + + ?REJECT?0 + ?ERROR?000 + + + + 2016-05-27T20:00:00 + + + + + 160527CH00T2Y4XY + + 00 + 20160527375204000060266 + + + 20.00 + CRDT + + + PMNT + + RCDT + ATXN + + + + + + Bernasconi Maria + + Place de la Gare 12 + CH/2502 Biel/Bienne + + + + + CH4444444444444444444 + + + + + + + + 000048358 + + Credit Suisse AG + + Paradeplatz 8 + 8070 Zürich + + + + + + MUSTER MITTEILUNG + + ?REJECT?0 + ?ERROR?000 + + + + 2016-05-27T20:00:00 + + + + + 160527CH00T3HDBA + + 00 + 20160527375204000060273 + + + 100.00 + CRDT + + + PMNT + + RCDT + AUTT + + + + + + Rutschmann Pia + + Marktgasse 28 + CH/9400 Rorschach + + + + + CH1111111111111111111 + + + + + + + POFICHBEXXX + POSTFINANCE AG + + MINGERSTRASSE 20 + 3030 BERNE + + + + + + MUSTER MITTEILUNG 1 + MUSTER MITTEILUNG 2 + MUSTER MITTEILUNG 3 + MUSTER MITTEILUNG 4 + + ?REJECT?0 + ?ERROR?000 + + + + 2016-05-27T20:00:00 + + + + + 160527CH00T3VT3Y + + 00 + 20160525117605000100096 + + + 100.00 + CRDT + + + PMNT + + CNTR + CDPT + + + + + 2.60 + + 1.80 + DBIT + false + + + 2 + + + + + 0.80 + DBIT + false + + + 5 + + + + + + + Bernasconi Maria + + Place de la Gare 12 + CH/2502 Biel/Bienne + + + + + + + POFICHBEXXX + POSTFINANCE AG + + MINGERSTRASSE 20 + 3030 BERNE + + + + + + + ?REJECT?0 + ?ERROR?008 + + + + 2016-05-25T20:00:00 + + + + + 160527CH00T3VTGB + + 00 + 20160525201601000100070 + + + 30.00 + CRDT + + + PMNT + + CNTR + CDPT + + + + + 2.30 + + 1.50 + DBIT + false + + + 2 + + + + + 0.80 + DBIT + false + + + 5 + + + + + + + Rutschmann Pia + + Marktgasse 28 + CH/9400 Rorschach + + + + + + + POFICHBEXXX + POSTFINANCE AG + + MINGERSTRASSE 20 + 3030 BERNE + + + + + + + ?REJECT?0 + ?ERROR?008 + + + + 2016-05-25T20:00:00 + + + + + 160527CH00T3VTGE + + 00 + 20160525863805000100101 + + + 50.00 + CRDT + + + PMNT + + CNTR + CDPT + + + + + 2.30 + + 1.50 + DBIT + false + + + 2 + + + + + 0.80 + DBIT + false + + + 5 + + + + + + + Bernasconi Maria + + Place de la Gare 12 + CH/2502 Biel/Bienne + + + + + + + POFICHBEXXX + POSTFINANCE AG + + MINGERSTRASSE 20 + 3030 BERNE + + + + + + MUSTER MITTEILUNG 1 + MUSTER MITTEILUNG 2 + + ?REJECT?0 + ?ERROR?000 + + + + 2016-05-25T20:00:00 + + + + + 160527CH00T2QNQZ + + 00 + 20160527375204000060263 + + + 20.00 + CRDT + + + PMNT + + RCDT + ATXN + + + + + + Rutschmann Pia + + Marktgasse 28 + CH/9400 Rorschach + + + + + CH1111111111111111111 + + + + + + + + 000048358 + + Credit Suisse AG + + Paradeplatz 8 + 8070 Zürich + + + + + + MUSTER MITTEILUNG 1 + MUSTER MITTEILUNG 2 + + ?REJECT?0 + ?ERROR?000 + + + + 2016-05-27T20:00:00 + + + + + 160527CH00T3HG80 + + 00 + 20160527375204000060271 + + + 250.00 + CRDT + + + PMNT + + RCDT + AUTT + + + + + + Rutschmann Pia + + Marktgasse + 28 + 9400 + Rorschach + CH + + + + + CH1111111111111111111 + + + + + + + POFICHBEXXX + POSTFINANCE AG + + MINGERSTRASSE 20 + 3030 BERNE + + + + + + MUSTER MITTEILUNG 1 + + ?REJECT?0 + ?ERROR?000 + + + + 2016-05-27T20:00:00 + + + + SAMMELGUTSCHRIFT ES VERARBEITUNG VOM 27.05.2016 PAKET ID: 160527CH000000R2 +
+ + 110.00 + CRDT + false + BOOK + +
2016-04-06
+
+ +
2016-03-23
+
+ 20160323007602726186629000000012 + + + PMNT + + ICDT + RRTN + + + + + + 3 + + + + MSG-RETURN-Test + 160406CH00NKZCKG + TEST002A11096JRG + + 20.00 + CRDT + + + PMNT + + ICDT + RRTN + + + + + + Max Mustermann + + Mustergasse + 8000 Zuerich + + + + + CH7408237100000000002 + + + + + + + + 082375 + + BANQUE PRIVEE BCP (SUISSE) SA + + Place du Molard 4 + 1211 Genève 3 + + + + + + Mitteilung Return Muster + + + 2016-04-06T20:00:00 + + + + NARR + + RETOUR VAL 23.03.2016 BEGUENSTIGTER UNBEKANNT + + + + + MSG-Return-Muster + 160406CH00NKSQ5N + Return104A11096849 + + 40.00 + CRDT + + + PMNT + + ICDT + RRTN + + + + + + Max Mustermann + + Mustergasse + 8000 Zuerich + + + + + CH7408237100000000002 + + + + + + + + 082375 + + BANQUE PRIVEE BCP (SUISSE) SA + + Place du Molard 4 + 1211 Genève 3 + + + + + + Return Mitteilung Muster + + + 2016-04-06T20:00:00 + + + + NARR + + RETOUR VAL 23.03.2016 BEGUENSTIGTER UNBEKANNT + + + + + MSG-Return-Muster + 160406CH00NKSQ5Q + Return005A11096C3M + + 50.00 + CRDT + + + PMNT + + ICDT + RRTN + + + + + + Max Mustermann + + Mustergasse + 8000 Zuerich + + + + + CH7408237100000000002 + + + + + + + + 082375 + + BANQUE PRIVEE BCP (SUISSE) SA + + Place du Molard 4 + 1211 Genève 3 + + + + + + Mitteilung Return Mitteilung + + + 2016-04-06T20:00:00 + + + + NARR + + RETOUR VAL 23.03.2016 BEGUENSTIGTER UNBEKANNT + + + + SAMMELGUTSCHRIFT RETOUREN VERARBEITUNG VOM 06.04.2016 PAKET ID: 160406CH0000005Q +
+ + 41107767420881932 + 24.00 + CRDT + false + BOOK + +
2016-04-10
+
+ +
2016-04-10
+
+ 20160410375244000000000000000081 + + + PMNT + + RDDT + PMDD + + + + + + 2 + + + + TST_201-2091644-9 + PMTINF-1CHDDBB3 + INSTR-CRDBRActSame-1B9C-CHDDBB2 + EndToEnd-CRDBRActSame-1B9C-CHDDBB2 + + 15.00 + CRDT + + + PMNT + + RDDT + PMDD + + + + + + Rutschmann Pia + + Marktgasse + 28 + 9400 + Rorschach + CH + + + + + CH4409000000495962779 + + + + + Mitteilung 132 + + + 2016-04-10T20:00:00 + + + + + TST_201-2091644-8 + PMTINF-1CHDDBB3 + INSTR-CRDBRActSame-1B9C-CHDDBB3 + EndToEnd-CRDBRActSame-1B9C-CHDDBB3 + + 9.00 + CRDT + + + PMNT + + RDDT + PMDD + + + + + + Bernasconi Maria + + 2502 + Biel/Bienne + CH + + + + + CH3709000000250094580 + + + + + Mitteilung 131 + + + 2016-04-10T20:00:00 + + + + GUTSCHRIFT CH-DD-BASISLASTSCHRIFT ID: 41107767420881932 REFERENZ-NR: PMTINF-1CHDDBB3 +
+ + 41107767420881932 + 56.56 + CRDT + false + BOOK + +
2016-04-10
+
+ +
2016-04-10
+
+ 20160410375244000000000000000071 + + + PMNT + + RDDT + PMDD + + + + + + 1 + + + + TST_201-13113-5 + PMT-ID-1268016165-01 + PMT-651727380907030005 + PMT-651727380907030005 + + 56.56 + CRDT + + + PMNT + + RDDT + PMDD + + + + + + Bernasconi Maria + + 2502 + Biel/Bienne + CH + + + + + CH3709000000250094580 + + + + + CORT + + + 2016-04-10T20:00:00 + + + + GUTSCHRIFT CH-DD-BASISLASTSCHRIFT ID: 41107767420881932 REFERENZ-NR: PMT-ID-1268016165-01 +
+ + 41107767420881932 + 6.00 + DBIT + true + BOOK + +
2016-04-10
+
+ +
2016-04-10
+
+ 20160410375244000000000000000082 + + + PMNT + + IDDT + PRDD + + + + + 0.70 + + 0.70 + DBIT + false + + + 9 + + + + + + + 1 + + + + TST_205-2091679-6 + PMTINF-2CHDDBB5 + INSTR-2B6C-CHDDBB5 + EndToEnd-2B6C-CHDDBB5 + + 6.00 + DBIT + + + PMNT + + RDDT + PMDD + + + + + 0.70 + + 0.70 + DBIT + false + + + 9 + + + + + + + Bernasconi Maria + + 2502 + Biel/Bienne + CH + + + + + CH3709000000250094580 + + + + + + + POFICHBEXXX + POSTFINANCE AG + + Mingerstrasse + 20 + 3030 + Bern + CH + + + + + + Mitteilung 128 + + + + MD06 + + Lastschrift-Widerspruch durch den Zahlungspflichtigen + + + + RÜCKBELASTUNG (REFUND) CHDD-BASISLASTSCHRIFT ID: 41107767420881932 REFERENZ-NR: PMTINF-2CHDDBB5 ZAHLUNGSPFLICHTIGER BERNASCONI MARIA MITTEILUNGEN: MD06 Lastschrift-Widerspruch durch den Zahlungspflichtigen +
+ + 41107767420881932 + 15.53 + DBIT + true + BOOK + +
2016-04-10
+
+ +
2016-04-10
+
+ 20160410375244000000000000000072 + + + PMNT + + RDDT + PRDD + + + + + 0.70 + + 0.70 + DBIT + false + + + 9 + + + + + + + 1 + + + + TST_205-28529-1 + SID-1-160309134053s4C + IID-1-1-160309134053s4C + E2E-1-1-16030913 + + 15.53 + DBIT + + + PMNT + + RDDT + PRDD + + + + + 0.70 + + 0.70 + DBIT + false + + + 9 + + + + + + + Rutschmann Pia + + Marktgasse + 28 + 9400 + Rorschach + CH + + + + + CH4409000000495962779 + + + + + + + POSTFINANCE AG + + Mingerstrasse + 20 + 3030 + Bern + CH + + + + + + Mitteilung 134- + + + + MD06 + + Lastschrift-Widerspruch durch den Zahlungspflichtigen + + + + RÜCKBELASTUNG (REFUND) CHDD-BASISLASTSCHRIFT ID: 41107767420881932 REFERENZ-NR: SID-1-160309134053s4C ZAHLUNGSPFLICHTIGER RUTSCHMANN PIA MITTEILUNGEN: MD06 Lastschrift-Widerspruch durch den Zahlungspflichtigen +
+
+
+
diff --git a/spec/fixtures/camt053/valid_example_v8.xml b/spec/fixtures/camt053/valid_example_v8.xml new file mode 100644 index 0000000..49a6ef0 --- /dev/null +++ b/spec/fixtures/camt053/valid_example_v8.xml @@ -0,0 +1,372 @@ + + + + + R39B4SA289HMZ6XA + 2023-06-20T18:43:23+02:00 + + + + UBSWCHZHREB + + + + + 1 + true + + SPS/2.0 + + + R30B4SA880HMZ9XA + 122 + 2023-06-20T18:43:23+02:00 + + 2023-06-20T00:00:00+02:00 + 2023-06-20T23:59:59+02:00 + + + + CH1111111111111111111 + + CHF + + ACME GmbH + + + + UBSWCHZH80A + UBS Switzerland AG + + CHE-116.303.292 MWST + VAT-ID + + + + + + + + OPBD + + + 82721.95 + CRDT +
+
2023-06-20
+ +
+ + + + CLBD + + + 84515.25 + CRDT +
+
2023-06-20
+ +
+ + + + CLAV + + + 83415.25 + CRDT +
+
2023-06-20
+ +
+ + 474.4 + CRDT + + BOOK + + +
2023-06-20
+
+ +
2023-06-20
+
+ 9999171ZC0173078 + + + PMNT + + RCDT + AUTT + + + + Z04 + + + + + 474.4 + + + + + + 9999181ZC0283079 + 80000004540000100095 + d0826d0f-a005-5e3b-b3dc-54da4ac7f0d0 + + 474.4 + CRDT + + + 474.4 + + + 474.4 + + + + + PMNT + + RCDT + AUTT + + + + Z04 + + + + + + Jon Doe + + Hofstrasse 2 + CH-8000 Zürich + + + + + + + + UBSWCHZH80A + + + + + INVOICE R77561 + + Payment + + + Payment +
+ + CH700033323323241701E + 88.85 + CRDT + + BOOK + + +
2023-06-20
+
+ +
2023-06-20
+
+ 2022172PH0003127 + + + PMNT + + RCDT + VCOM + + + + A90 + + + + + 1b30f9941c8e459f9b0a7b11137b3720 + 8d8075ce49134553a261033714278400 + 1 + 88.85 + CRDT + + + + 0123171DO5126811 + 435A9287E088BDB1D97FAABD181C70C8 + + 88.85 + CRDT + + + 88.85 + + + 88.85 + + + + + PMNT + + RCDT + VCOM + + + + A90 + + + + + + Finanz AG + + + + + Finanz AG + + + + + + + + + SCOR + + + RF38000000000000000000552 + + + + Credit Creditor Reference + + + Credit Creditor Reference +
+ + 19.69 + DBIT + + BOOK + + +
2023-06-20
+
+ +
2023-06-20
+
+ 0124571DN6195349 + + + PMNT + + ICDT + AUTT + + + + Z44 + + + + + 19.69 + + + 19.69 + + + + + + 01222ABC + 0123171DN6095310 + 01222ABC-HAF + 373502949-1208481 + 1208481 + + 19.69 + DBIT + + + 19.69 + + + 19.69 + + + + + PMNT + + ICDT + AUTT + + + + Z44 + + + + + + ACME GmbH + + + + + DHL Express (Schweiz) AG + + CH + Hochstrasse 5 + 4052 Basel + + + + + + CH2222222222222222222 + + + + + + + UBSWCHZH80A + + + + + + + + + QRR + + + 000000270148751802165444888 + + + + e-banking Order // FILETRANSFER + + + e-banking Order // FILETRANSFER +
+ +
+
+
\ No newline at end of file diff --git a/spec/fixtures/camt053/valid_example_with_batch.xml b/spec/fixtures/camt053/valid_example_with_batch.xml new file mode 100644 index 0000000..530d206 --- /dev/null +++ b/spec/fixtures/camt053/valid_example_with_batch.xml @@ -0,0 +1,261 @@ + + + + + 201906240000000_2019062111111111 + 2019-06-24T14:39:03.758+02:00 + + + 2019062434566666666666666 + 2019-06-24T14:39:03.923+02:00 + + 2019-01-01T00:00:00 + 2019-06-23T23:59:59 + + + + NL18INGB00012345678 + + + CACC + + EUR + + + INGBNL2A + + + + + + + PRCD + + + 7329.80 + CRDT +
+
2018-12-31
+ +
+ + + + OPBD + + + 7329.80 + CRDT +
+
2019-01-01
+ +
+ + + + CLBD + + + 23038.20 + CRDT +
+
2019-06-23
+ +
+ + + + CLAV + + + 23038.20 + CRDT +
+
2019-06-23
+ +
+ + + + FWAV + + + 23038.20 + CRDT +
+
2019-06-24
+ +
+ + + + FWAV + + + 23038.20 + CRDT +
+
2019-06-25
+ +
+ + + 35 + 41136.40 + + + 9 + 28422.40 + + + 26 + 12714.00 + + + + 032222222222222221000000001 + 4753.74 + DBIT + BOOK + +
2019-05-21
+
+ +
2019-05-21
+
+ 68047a4871ab34285b5555555502010c + + + PMNT + + ICDT + ESCT + + + + 00200 + ING Group + + + + + O0OpeAYTkhjerKu3eE9asw + 02453b1e17c11241073a777ad9c273b4149 + 3 + + +
+ + 032019164412145727005003002 + 2688.00 + CRDT + BOOK + +
2019-06-13
+
+ +
2019-06-13
+
+ s8ecaaf0f96f492f603167c748a0b1c6 + + + PMNT + + RCDT + ESCT + + + + 00100 + ING Group + + + + + + GBSXX21170607391766562 + + + + TEST + + + + NL86INGB00066666666 + + + General + + EUR + + + + + + INGBNL2A + + + + + TERUGGAAF + + + 2019-06-12T00:00:00 + + + +
+ + 032019169492039060000000002 + 5000.00 + CRDT + BOOK + +
2019-06-18
+
+ +
2019-06-18
+
+ d36e51a0ff7e4561b11c2b1c5d79d9ea + + + PMNT + + RCDT + ESCT + + + + 00100 + ING Group + + + + + + + Bedrijf BV + + + + NL11INGB0001234567 + + + General + + EUR + + + + + + INGBNL2A + + + + + +
+
+
+
diff --git a/spec/fixtures/camt053/valid_example_with_datetime.xml b/spec/fixtures/camt053/valid_example_with_datetime.xml new file mode 100644 index 0000000..d6192d6 --- /dev/null +++ b/spec/fixtures/camt053/valid_example_with_datetime.xml @@ -0,0 +1,112 @@ + + + + + 1234567-1234567891000 + 2023-07-06T10:27:07.213294607Z + + + 7301750-38631543-1688639227213 + 2023-07-06T10:27:07.213294607Z + + 2022-07-07T00:00:00+02:00 + 2023-07-07T00:00:00+02:00 + + + + + 11111111 + + Wise Account ID + + Wise Payments Ltd. + + + USD + + Max Muster + + + ADDR + + 4410 + Liestal + Beispielstrasse 10 + + + + + 1111111 + + CUST + + + + + + + + Wise Payments Ltd. + + + ADDR + + E1 6JJ + London + 6th Floor, The Tea Building, 56 Shoreditch High Street + + + + + + + + CLBD + + + 606.80 + CRDT +
+ 2023-07-07T00:00:00+02:00 +
+
+ + + 14 + 606.80 + + 606.80 + CRDT + + + + 4 + 1115.92 + + + 10 + -509.12 + + + + Test Buchung + 195.86 + CRDT + + BOOK + + + 2023-06-21T12:35:33.115965+00:00 + + + + TRANSFER-123456789 + + + + 4.14 + + +
+
+
diff --git a/spec/fixtures/camt053/valid_example_with_debit.xml b/spec/fixtures/camt053/valid_example_with_debit.xml new file mode 100755 index 0000000..9b587c4 --- /dev/null +++ b/spec/fixtures/camt053/valid_example_with_debit.xml @@ -0,0 +1,368 @@ + + + + + + 053D2013-12-27T22:05:03.0N130000005 + 2013-12-27T22:04:52.0+01:00 + + 1 + true + + + + 0352C5320131227220503 + 130000005 + 2013-12-27T22:04:52.0+01:00 + + + DE14740618130000033626 + + EUR + + Testkonto Nummer 1 + + + + GENODEF1PFK + VR-Bank Rottal-Inn eG + + DE 129267947 + UmsStId + + + + + + + + PRCD + + + 33.06 + DBIT +
+
2013-12-27
+ +
+ + + + CLBD + + + 23.06 + CRDT +
+
2013-12-27
+ +
+ + 2.00 + DBIT + BOOK + +
2013-12-27
+
+ +
2013-12-27
+
+ 2013122710583450000 + + + + + BankReference + EndToEndReference + MandateReference + PaymentIdentification + UniqueTransactionId + + AdditionalTransactionInformation + + + NTRF+020 + ZKA + + + + + Wayne Enterprises + + + + DE24302201900609832118 + + + + + + + CreditorIdentifier + + + + Testkonto Nummer 2 + + Berlin + Infinite Loop 2 + 12345 + + + + + DE09300606010012345671 + + + CACC + + + + + + + DAAEDEDDXXX + + + ABCDEF + + 1232344234234 + + + + + + DAAEDEDDXXX + + + ABCDEF + + 123456789 + + Bank + + Infinite Loop 1 + Berlin + + + + + + TEST BERWEISUNG MITTELS BLZUND KONTONUMMER - DTA + + + + Überweisungs-Gutschrift; GVC: SEPA Credit Transfer (Einzelbuchung-Haben) +
+ + 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 + + + +
+
+
+
diff --git a/spec/fixtures/camt053/valid_example_with_instdamt.xml b/spec/fixtures/camt053/valid_example_with_instdamt.xml new file mode 100755 index 0000000..91b2a11 --- /dev/null +++ b/spec/fixtures/camt053/valid_example_with_instdamt.xml @@ -0,0 +1,138 @@ + + + + + MSG-C053-190720325942-01 + 2019-06-20T00:00:00 + + Jon Doe + + + 1 + true + + PRODUCTIVE + + + STM-C053-190720325942-06 + 1 + 2019-06-20T00:00:00 + + 2019-05-13T00:00:00 + 2019-06-19T00:00:00 + + + + CH9788761065025111111 + + + Jon Doe + + + + 8402342 + + + + + + + + + OPBD + + + 100 + CRDT +
+
2019-05-13
+ +
+ + + + CLBD + + + 4500.00 + CRDT +
+
2019-05-13
+ +
+ + + 1 + 4500.00 + + + 1 + 4500.00 + CRDT + + + * + + * + OTHR + + + + + + + 1340044232 + 4500.00 + CRDT + BOOK + +
2019-05-13
+
+ +
2019-05-13
+
+ 1340044232 + + + * + + * + OTHR + + + + + + + + 4500.00 + + + 0.04 + + + IBS + 0.03 + + + + + * + + * + OTHR + + + + + + Praxis am Weiher + + Zahlungseingang / Ref.-Nr. 99999 + + + Zahlungseingang / Ref.-Nr. 99999 +
+
+
+
diff --git a/spec/fixtures/camt053/valid_example_with_other_id.xml b/spec/fixtures/camt053/valid_example_with_other_id.xml new file mode 100755 index 0000000..a089617 --- /dev/null +++ b/spec/fixtures/camt053/valid_example_with_other_id.xml @@ -0,0 +1,40 @@ + + + + + + 053D2013-12-27T22:05:03.0N130000005 + 2013-12-27T22:04:52.0+01:00 + + 1 + true + + + + 0352C5320131227220503 + 130000005 + 2013-12-27T22:04:52.0+01:00 + + + + ABCDE1234 + + + EUR + + Testkonto Nummer 1 + + + + GENODEF1PFK + VR-Bank Rottal-Inn eG + + DE 129267947 + UmsStId + + + + + + + diff --git a/spec/fixtures/camt053/valid_namespace.xml b/spec/fixtures/camt053/valid_namespace.xml new file mode 100644 index 0000000..bedcfb1 --- /dev/null +++ b/spec/fixtures/camt053/valid_namespace.xml @@ -0,0 +1,4 @@ + + + + diff --git a/spec/fixtures/camt054/valid_example.xml b/spec/fixtures/camt054/valid_example.xml new file mode 100644 index 0000000..c55520d --- /dev/null +++ b/spec/fixtures/camt054/valid_example.xml @@ -0,0 +1,437 @@ + + + + + 20160410375204000131033 + 2016-04-10T22:04:06 + + 1 + true + + Productive + + + 20160410375204000131032 + 2016-04-10T22:04:06 + + 2016-04-10T00:00:00 + 2016-04-10T23:59:59 + + + + CH0309000000250090342 + + + Robert Schneider SA Grands magasins Biel/Bienne + + + + 41107767420881932 + 24.00 + CRDT + false + BOOK + +
2016-04-10
+
+ +
2016-04-10
+
+ 20160410375244000000000000000081 + + + PMNT + + RDDT + PMDD + + + + + + 2 + + + + TST_201-2091644-9 + PMTINF-1CHDDBB3 + INSTR-CRDBRActSame-1B9C-CHDDBB2 + EndToEnd-CRDBRActSame-1B9C-CHDDBB2 + + 15.00 + CRDT + + + PMNT + + RDDT + PMDD + + + + + + Rutschmann Pia + + Marktgasse + 28 + 9400 + Rorschach + CH + + + + + CH4409000000495962779 + + + + + Mitteilung 132 + + + 2016-04-10T20:00:00 + + + + + TST_201-2091644-8 + PMTINF-1CHDDBB3 + INSTR-CRDBRActSame-1B9C-CHDDBB3 + EndToEnd-CRDBRActSame-1B9C-CHDDBB3 + + 9.00 + CRDT + + + PMNT + + RDDT + PMDD + + + + + + Bernasconi Maria + + 2502 + Biel/Bienne + CH + + + + + CH3709000000250094580 + + + + + Mitteilung 131 + + + 2016-04-10T20:00:00 + + + + GUTSCHRIFT CH-DD-BASISLASTSCHRIFT ID: 41107767420881932 REFERENZ-NR: PMTINF-1CHDDBB3 +
+ + 41107767420881932 + 56.56 + CRDT + false + BOOK + +
2016-04-10
+
+ +
2016-04-10
+
+ 20160410375244000000000000000071 + + + PMNT + + RDDT + PMDD + + + + + + 1 + + + + TST_201-13113-5 + PMT-ID-1268016165-01 + PMT-651727380907030005 + PMT-651727380907030005 + + 56.56 + CRDT + + + PMNT + + RDDT + PMDD + + + + + + Bernasconi Maria + + 2502 + Biel/Bienne + CH + + + + + CH3709000000250094580 + + + + + CORT + + + 2016-04-10T20:00:00 + + + + GUTSCHRIFT CH-DD-BASISLASTSCHRIFT ID: 41107767420881932 REFERENZ-NR: PMT-ID-1268016165-01 +
+ + 41107767420881932 + 6.00 + DBIT + true + BOOK + +
2016-04-10
+
+ +
2016-04-10
+
+ 20160410375244000000000000000082 + + + PMNT + + IDDT + PRDD + + + + + 0.70 + + 0.70 + DBIT + false + + + 9 + + + + + + + 1 + + + + TST_205-2091679-6 + PMTINF-2CHDDBB5 + INSTR-2B6C-CHDDBB5 + EndToEnd-2B6C-CHDDBB5 + + 6.00 + DBIT + + + PMNT + + RDDT + PMDD + + + + + 0.70 + + 0.70 + DBIT + false + + + 9 + + + + + + + Bernasconi Maria + + 2502 + Biel/Bienne + CH + + + + + CH3709000000250094580 + + + + + + + POFICHBEXXX + POSTFINANCE AG + + Mingerstrasse + 20 + 3030 + Bern + CH + + + + + + Mitteilung 128 + + + + MD06 + + Lastschrift-Widerspruch durch den Zahlungspflichtigen + + + + RÜCKBELASTUNG (REFUND) CHDD-BASISLASTSCHRIFT ID: 41107767420881932 REFERENZ-NR: PMTINF-2CHDDBB5 ZAHLUNGSPFLICHTIGER BERNASCONI MARIA MITTEILUNGEN: MD06 Lastschrift-Widerspruch durch den Zahlungspflichtigen +
+ + 41107767420881932 + 15.53 + DBIT + true + BOOK + +
2016-04-10
+
+ +
2016-04-10
+
+ 20160410375244000000000000000072 + + + PMNT + + RDDT + PRDD + + + + + 0.70 + + 0.70 + DBIT + false + + + 9 + + + + + + + 1 + + + + TST_205-28529-1 + SID-1-160309134053s4C + IID-1-1-160309134053s4C + E2E-1-1-16030913 + + 15.53 + DBIT + + + PMNT + + RDDT + PRDD + + + + + 0.70 + + 0.70 + DBIT + false + + + 9 + + + + + + + Rutschmann Pia + + Marktgasse + 28 + 9400 + Rorschach + CH + + + + + CH4409000000495962779 + + + + + + + POSTFINANCE AG + + Mingerstrasse + 20 + 3030 + Bern + CH + + + + + + Mitteilung 134- + + + + MD06 + + Lastschrift-Widerspruch durch den Zahlungspflichtigen + + + + RÜCKBELASTUNG (REFUND) CHDD-BASISLASTSCHRIFT ID: 41107767420881932 REFERENZ-NR: SID-1-160309134053s4C ZAHLUNGSPFLICHTIGER RUTSCHMANN PIA MITTEILUNGEN: MD06 Lastschrift-Widerspruch durch den Zahlungspflichtigen +
+
+
+
diff --git a/spec/fixtures/general/invalid_namespace.xml b/spec/fixtures/general/invalid_namespace.xml new file mode 100644 index 0000000..6e769b3 --- /dev/null +++ b/spec/fixtures/general/invalid_namespace.xml @@ -0,0 +1,4 @@ + + + + diff --git a/spec/lib/sepa_file_parser/camt052/base_spec.rb b/spec/lib/sepa_file_parser/camt052/base_spec.rb new file mode 100644 index 0000000..d0f660a --- /dev/null +++ b/spec/lib/sepa_file_parser/camt052/base_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe SepaFileParser::Camt052::Base do + + context 'initialization' do + after do + SepaFileParser::File.parse 'spec/fixtures/camt052/valid_example.xml' + end + + specify { expect(SepaFileParser::GroupHeader).to receive(:new).and_call_original } + specify { expect(SepaFileParser::Camt052::Report).to receive(:new).and_call_original } + end + + let(:camt) { SepaFileParser::File.parse 'spec/fixtures/camt052/valid_example.xml' } + specify { expect(camt.group_header).to_not be_nil } + specify { expect(camt.reports).to_not eq([]) } + specify { expect(camt.xml_data).to_not be_nil } + +end diff --git a/spec/lib/sepa_file_parser/camt052/report_spec.rb b/spec/lib/sepa_file_parser/camt052/report_spec.rb new file mode 100644 index 0000000..9325f5c --- /dev/null +++ b/spec/lib/sepa_file_parser/camt052/report_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe SepaFileParser::Camt052::Report do + let(:camt) { SepaFileParser::File.parse('spec/fixtures/camt052/valid_example.xml') } + let(:reports) { camt.reports } + let(:ex_rpt) { camt.reports[0] } + + specify { expect(reports).to all(be_kind_of(described_class)) } + specify { expect(ex_rpt.identification).to eq("0352C5220131227110203") } + specify { expect(ex_rpt.generation_date).to be_kind_of(Time) } + specify { expect(ex_rpt.account).to be_kind_of(SepaFileParser::Account) } + specify { expect(ex_rpt.entries).to be_kind_of(Array) } + + specify { expect(ex_rpt.opening_balance).to be_kind_of(SepaFileParser::AccountBalance) } + specify { expect(ex_rpt.closing_balance).to be_kind_of(SepaFileParser::AccountBalance) } + + specify { expect(ex_rpt.identification).to eq("0352C5220131227110203") } + + specify { expect(ex_rpt.from_date_time).to be_nil } + specify { expect(ex_rpt.to_date_time).to be_nil } + + context 'Rpt/FrToDt' do + let(:camt) { SepaFileParser::File.parse('spec/fixtures/camt052/valid_example_with_dates.xml') } + + specify { expect(ex_rpt.from_date_time).to be_kind_of(Time) } + specify { expect(ex_rpt.from_date_time).to eq(Time.new(2013, 1, 1, 0, 0, 0)) } + + specify { expect(ex_rpt.to_date_time).to be_kind_of(Time) } + specify { expect(ex_rpt.to_date_time).to eq(Time.new(2019, 8, 9, 0, 0, 0)) } + end + + specify { expect(camt.xml_data).to_not be_nil } +end diff --git a/spec/lib/sepa_file_parser/camt053/base_spec.rb b/spec/lib/sepa_file_parser/camt053/base_spec.rb new file mode 100644 index 0000000..c957339 --- /dev/null +++ b/spec/lib/sepa_file_parser/camt053/base_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe SepaFileParser::Camt053::Base do + + context 'version 2' do + context 'initialization' do + after do + SepaFileParser::File.parse 'spec/fixtures/camt053/valid_example.xml' + end + + specify { expect(SepaFileParser::GroupHeader).to receive(:new).and_call_original } + specify { expect(SepaFileParser::Camt053::Statement).to receive(:new).and_call_original } + end + + let(:camt) { SepaFileParser::File.parse 'spec/fixtures/camt053/valid_example.xml' } + specify { expect(camt.group_header).to_not be_nil } + specify { expect(camt.statements).to_not eq([]) } + specify { expect(camt.xml_data).to_not be_nil } + end + + context 'version 4' do + context 'initialization' do + after do + SepaFileParser::File.parse 'spec/fixtures/camt053/valid_example_v4.xml' + end + + specify { expect(SepaFileParser::GroupHeader).to receive(:new).and_call_original } + specify { expect(SepaFileParser::Camt053::Statement).to receive(:new).and_call_original } + end + + let(:camt) { SepaFileParser::File.parse 'spec/fixtures/camt053/valid_example_v4.xml' } + specify { expect(camt.group_header).to_not be_nil } + specify { expect(camt.statements).to_not eq([]) } + specify { expect(camt.xml_data).to_not be_nil } + end +end diff --git a/spec/lib/sepa_file_parser/camt053/statement_spec.rb b/spec/lib/sepa_file_parser/camt053/statement_spec.rb new file mode 100644 index 0000000..7b6fa00 --- /dev/null +++ b/spec/lib/sepa_file_parser/camt053/statement_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe SepaFileParser::Camt053::Statement do + context 'version 2' do + let(:camt) { SepaFileParser::File.parse('spec/fixtures/camt053/valid_example.xml') } + let(:statements) { camt.statements } + let(:ex_stmt) { camt.statements[0] } + + specify { expect(statements).to all(be_kind_of(described_class)) } + specify { expect(ex_stmt.identification).to eq("0352C5320131227220503") } + specify { expect(ex_stmt.generation_date).to be_kind_of(Time) } + specify { expect(ex_stmt.from_date_time).to be_nil } + specify { expect(ex_stmt.to_date_time).to be_nil } + specify { expect(ex_stmt.account).to be_kind_of(SepaFileParser::Account) } + specify { expect(ex_stmt.entries).to be_kind_of(Array) } + specify { expect(ex_stmt.electronic_sequence_number).to eq('130000005') } + + specify { expect(ex_stmt.opening_balance).to be_kind_of(SepaFileParser::AccountBalance) } + specify { expect(ex_stmt.closing_balance).to be_kind_of(SepaFileParser::AccountBalance) } + + specify { expect(ex_stmt.identification).to eq("0352C5320131227220503") } + + specify { expect(ex_stmt.xml_data).to_not be_nil } + end + + context 'version 4' do + let(:camt) { SepaFileParser::File.parse('spec/fixtures/camt053/valid_example_v4.xml') } + let(:statements) { camt.statements } + let(:ex_stmt) { camt.statements[0] } + let(:ex_ntry) { ex_stmt.entries[0] } + let(:ex_ntry_chrgs) { ex_stmt.entries[0] } + + specify { expect(ex_ntry.charges).to be_kind_of(SepaFileParser::Charges)} + + specify { expect(ex_stmt.xml_data).to_not be_nil } + end + + context 'version 8' do + let(:camt) { SepaFileParser::File.parse('spec/fixtures/camt053/valid_example_v8.xml') } + + let(:statements) { camt.statements } + let(:ex_stmt) { camt.statements[0] } + let(:ex_ntry) { ex_stmt.entries[0] } + let(:ex_ntry_chrgs) { ex_stmt.entries[0] } + + specify { expect(ex_ntry.charges).to be_kind_of(SepaFileParser::Charges) } + specify { expect(ex_stmt.identification).to eq("R30B4SA880HMZ9XA") } + specify { expect(ex_stmt.generation_date).to be_kind_of(Time) } + specify { expect(ex_stmt.from_date_time).to eq(Time.parse("2023-06-20 00:00:00 +0200")) } + specify { expect(ex_stmt.to_date_time).to eq(Time.parse("2023-06-20 23:59:59 +0200")) } + specify { expect(ex_stmt.account).to be_kind_of(SepaFileParser::Account) } + specify { expect(ex_stmt.entries).to be_kind_of(Array) } + specify { expect(ex_stmt.electronic_sequence_number).to eq("122") } + + specify { expect(ex_stmt.opening_balance).to be_kind_of(SepaFileParser::AccountBalance) } + specify { expect(ex_stmt.closing_balance).to be_kind_of(SepaFileParser::AccountBalance) } + + specify { expect(ex_stmt.xml_data).to_not be_nil } + end +end diff --git a/spec/lib/sepa_file_parser/camt054/base_spec.rb b/spec/lib/sepa_file_parser/camt054/base_spec.rb new file mode 100644 index 0000000..98de40b --- /dev/null +++ b/spec/lib/sepa_file_parser/camt054/base_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe SepaFileParser::Camt054::Base do + context 'initialization' do + after do + SepaFileParser::File.parse 'spec/fixtures/camt054/valid_example.xml' + end + + specify { expect(SepaFileParser::GroupHeader).to receive(:new).and_call_original } + specify { expect(SepaFileParser::Camt054::Notification).to receive(:new).and_call_original } + end + + let(:camt) { SepaFileParser::File.parse 'spec/fixtures/camt054/valid_example.xml' } + specify { expect(camt.group_header).to_not be_nil } + specify { expect(camt.notifications).to_not eq([]) } + specify { expect(camt.xml_data).to_not be_nil } + +end diff --git a/spec/lib/sepa_file_parser/camt054/notification_spec.rb b/spec/lib/sepa_file_parser/camt054/notification_spec.rb new file mode 100644 index 0000000..3da8f66 --- /dev/null +++ b/spec/lib/sepa_file_parser/camt054/notification_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe SepaFileParser::Camt054::Notification do + let(:camt) { SepaFileParser::File.parse('spec/fixtures/camt054/valid_example.xml') } + let(:notifications) { camt.notifications } + let(:ex_ntfcn) { camt.notifications[0] } + + specify { expect(notifications).to all(be_kind_of(described_class)) } + specify { expect(ex_ntfcn.identification).to eq("20160410375204000131032") } + specify { expect(ex_ntfcn.generation_date).to be_kind_of(Time) } + specify { expect(ex_ntfcn.from_date_time).to be_kind_of(Time) } + specify { expect(ex_ntfcn.to_date_time).to be_kind_of(Time) } + specify { expect(ex_ntfcn.account).to be_kind_of(SepaFileParser::Account) } + specify { expect(ex_ntfcn.entries).to be_kind_of(Array) } + specify { expect(ex_ntfcn.xml_data).to_not be_nil } + +end diff --git a/spec/lib/sepa_file_parser/file_spec.rb b/spec/lib/sepa_file_parser/file_spec.rb new file mode 100644 index 0000000..e488d17 --- /dev/null +++ b/spec/lib/sepa_file_parser/file_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe SepaFileParser::File do + context "parse" do + it "raises an exception if the namespace/format is unknown" do + expect{ + described_class.parse 'spec/fixtures/general/invalid_namespace.xml' + }.to raise_exception(SepaFileParser::Errors::UnsupportedNamespaceError, 'urn:iso:std:iso:20022:tech:xsd:camt.053.001.03') + end + + it "does not raise an exception for a valid 052 namespace" do + expect(SepaFileParser::Format052::Base).to receive(:new) + described_class.parse 'spec/fixtures/052/valid_namespace.xml' + end + + it "does not raise an exception for a valid 053 namespace" do + expect(SepaFileParser::Format053::Base).to receive(:new) + described_class.parse 'spec/fixtures/053/valid_namespace.xml' + end + end +end diff --git a/spec/lib/sepa_file_parser/general/account_balance_spec.rb b/spec/lib/sepa_file_parser/general/account_balance_spec.rb new file mode 100644 index 0000000..12d2ca4 --- /dev/null +++ b/spec/lib/sepa_file_parser/general/account_balance_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe SepaFileParser::AccountBalance do + let(:camt) { SepaFileParser::File.parse('spec/fixtures/camt053/valid_example.xml') } + let(:statements) { camt.statements } + let(:ex_stmt) { camt.statements[0] } + subject { ex_stmt.opening_balance } + + specify { expect(subject.currency).to eq "EUR" } + specify { expect(subject.date).to eq Date.new(2013, 12, 27) } + specify { expect(subject.sign).to eq 1 } + specify { expect(subject.credit?).to be_truthy } + specify { expect(subject.amount).to eq BigDecimal("33.06") } + specify { expect(subject.amount_in_cents).to eq(3306) } + specify { expect(subject.signed_amount).to eq BigDecimal("33.06") } + specify { expect(subject.to_h).to eq({ + 'amount' => BigDecimal('33.06'), + 'amount_in_cents' => 3306, + 'sign' => 1 + }) } + + context 'debit' do + let(:camt) { SepaFileParser::File.parse('spec/fixtures/camt053/valid_example_with_debit.xml') } + let(:statements) { camt.statements } + let(:ex_stmt) { camt.statements[0] } + subject { ex_stmt.opening_balance } + + specify { expect(subject.currency).to eq "EUR" } + specify { expect(subject.date).to eq Date.new(2013, 12, 27) } + specify { expect(subject.sign).to eq -1 } + specify { expect(subject.credit?).to be_falsey } + specify { expect(subject.amount).to eq BigDecimal("33.06") } + specify { expect(subject.amount_in_cents).to eq(3306) } + specify { expect(subject.signed_amount).to eq BigDecimal("-33.06") } + specify { expect(subject.to_h).to eq({ + 'amount' => BigDecimal('33.06'), + 'amount_in_cents' => 3306, + 'sign' => -1 + }) } + end +end diff --git a/spec/lib/sepa_file_parser/general/account_spec.rb b/spec/lib/sepa_file_parser/general/account_spec.rb new file mode 100644 index 0000000..6061633 --- /dev/null +++ b/spec/lib/sepa_file_parser/general/account_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe SepaFileParser::Account do + let(:camt) { SepaFileParser::File.parse('spec/fixtures/camt053/valid_example.xml') } + let(:statements) { camt.statements } + let(:ex_stmt) { camt.statements[0] } + let(:account) { ex_stmt.account } + + specify { expect(account.iban).to eq("DE14740618130000033626") } + specify { expect(account.account_number).to eq("DE14740618130000033626") } + specify { expect(account.bic).to eq("GENODEF1PFK") } + specify { expect(account.bank_name).to eq("VR-Bank Rottal-Inn eG") } + specify { expect(account.currency).to eq("EUR") } + specify { expect(account.xml_data).to_not be_nil } + + context 'with Other/Id as account_number' do + let(:camt) { SepaFileParser::File.parse('spec/fixtures/camt053/valid_example_with_other_id.xml') } + + specify { expect(account.other_id).to eq("ABCDE1234") } + specify { expect(account.account_number).to eq("ABCDE1234") } + end + + context "version 8" do + let(:camt) { SepaFileParser::File.parse('spec/fixtures/camt053/valid_example_v8.xml') } + + specify { expect(account.iban).to eq("CH1111111111111111111") } + specify { expect(account.account_number).to eq("CH1111111111111111111") } + specify { expect(account.bic).to eq("UBSWCHZH80A") } + specify { expect(account.bank_name).to eq("UBS Switzerland AG") } + specify { expect(account.currency).to eq("CHF") } + end +end diff --git a/spec/lib/sepa_file_parser/general/batch_detail_spec.rb b/spec/lib/sepa_file_parser/general/batch_detail_spec.rb new file mode 100644 index 0000000..1809964 --- /dev/null +++ b/spec/lib/sepa_file_parser/general/batch_detail_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe SepaFileParser::BatchDetail do + let(:camt) { SepaFileParser::File.parse('spec/fixtures/camt053/valid_example_with_batch.xml') } + let(:statements) { camt.statements } + let(:ex_stmt) { statements[0] } + let(:entries) { ex_stmt.entries } + let(:ex_entry) { entries[0] } + let(:batch_detail) { ex_entry.batch_detail } + + specify { expect(batch_detail.payment_information_identification).to eq("O0OpeAYTkhjerKu3eE9asw") } + specify { expect(batch_detail.number_of_transactions).to eq("3") } + specify { expect(batch_detail.msg_id).to eq('02453b1e17c11241073a777ad9c273b4149') } + specify { expect(batch_detail.xml_data).to_not be_nil } +end diff --git a/spec/lib/sepa_file_parser/general/charges_spec.rb b/spec/lib/sepa_file_parser/general/charges_spec.rb new file mode 100644 index 0000000..a490a80 --- /dev/null +++ b/spec/lib/sepa_file_parser/general/charges_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe SepaFileParser::Charges do + let(:camt) { SepaFileParser::File.parse('spec/fixtures/camt053/valid_example_v4.xml') } + let(:entries) { camt.statements.first.entries } + let(:ex_charges) { entries[6].charges } + + specify { expect(entries.map(&:charges)).to all(be_kind_of(described_class)) } + specify { expect(ex_charges.total_charges_and_tax_amount).to eq(BigDecimal('5.2')) } + specify { expect(ex_charges.xml_data).to_not be_nil } +end diff --git a/spec/lib/sepa_file_parser/general/creditor_spec.rb b/spec/lib/sepa_file_parser/general/creditor_spec.rb new file mode 100644 index 0000000..66e34eb --- /dev/null +++ b/spec/lib/sepa_file_parser/general/creditor_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe SepaFileParser::Creditor do + let(:camt) { SepaFileParser::File.parse('spec/fixtures/camt053/valid_example.xml') } + let(:statements) { camt.statements } + let(:ex_stmt) { statements[0] } + let(:entries) { ex_stmt.entries } + let(:ex_entry) { entries[0] } + let(:transactions) { ex_entry.transactions } + let(:ex_transaction) { transactions[0] } + let(:creditor) { ex_transaction.creditor } + + specify { expect(creditor.name).to eq("Testkonto Nummer 2") } + specify { expect(creditor.iban).to eq("DE09300606010012345671") } + specify { expect(creditor.bic).to eq("DAAEDEDDXXX") } + specify { expect(creditor.bank_name).to eq("Bank") } + specify { expect(creditor.xml_data).to_not be_nil } + + context "version 8" do + let(:camt) { SepaFileParser::File.parse('spec/fixtures/camt053/valid_example_v8.xml') } + let(:ex_entry) { entries[2] } + + specify { expect(creditor.name).to eq("DHL Express (Schweiz) AG") } + specify { expect(creditor.bic).to eq("UBSWCHZH80A") } + end + + context "with address" do + let(:camt) { SepaFileParser::File.parse('spec/fixtures/camt053/valid_example_with_debit.xml') } + + specify { expect(creditor.name).to eq("Testkonto Nummer 2") } + specify { expect(creditor.postal_address.lines).to eq(["Berlin", "Infinite Loop 2", "12345"]) } + end +end diff --git a/spec/lib/sepa_file_parser/general/debitor_spec.rb b/spec/lib/sepa_file_parser/general/debitor_spec.rb new file mode 100644 index 0000000..afe1243 --- /dev/null +++ b/spec/lib/sepa_file_parser/general/debitor_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe SepaFileParser::Debitor do + let(:camt) { SepaFileParser::File.parse('spec/fixtures/camt053/valid_example.xml') } + let(:statements) { camt.statements } + let(:ex_stmt) { statements[0] } + let(:entries) { ex_stmt.entries } + let(:ex_entry) { entries[0] } + let(:transactions) { ex_entry.transactions } + let(:ex_transaction) { transactions[0] } + let(:debitor) { ex_transaction.debitor } + + specify { expect(debitor.name).to eq("Wayne Enterprises") } + specify { expect(debitor.iban).to eq("DE24302201900609832118") } + specify { expect(debitor.bic).to eq("DAAEDEDDXXX") } + specify { expect(debitor.bank_name).to eq("") } + specify { expect(debitor.xml_data).to_not be_nil } + + context "version 8" do + let(:camt) { SepaFileParser::File.parse('spec/fixtures/camt053/valid_example_v8.xml') } + + specify { expect(debitor.name).to eq("Jon Doe") } + specify { expect(debitor.bic).to eq("UBSWCHZH80A") } + end + + context "with address" do + let(:camt) { SepaFileParser::File.parse('spec/fixtures/camt053/valid_example_v8.xml') } + + specify { expect(debitor.name).to eq("Jon Doe") } + specify { expect(debitor.postal_address.lines).to eq(["Hofstrasse 2", "CH-8000 Zürich"]) } + end +end diff --git a/spec/lib/sepa_file_parser/general/entry_spec.rb b/spec/lib/sepa_file_parser/general/entry_spec.rb new file mode 100644 index 0000000..4a37a8f --- /dev/null +++ b/spec/lib/sepa_file_parser/general/entry_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe SepaFileParser::Entry do + let(:camt) { SepaFileParser::File.parse('spec/fixtures/camt053/valid_example.xml') } + let(:statements) { camt.statements } + let(:ex_stmt) { camt.statements[0] } + let(:entries) { ex_stmt.entries } + let(:ex_entry) { ex_stmt.entries[0] } + + specify { expect(entries).to all(be_kind_of(described_class)) } + + context '#amount' do + specify { expect(ex_entry.amount).to be_kind_of(BigDecimal) } + specify { expect(ex_entry.amount).to eq(BigDecimal('2')) } + specify { expect(ex_entry.amount_in_cents).to eq(200) } + end + + specify { expect(ex_entry.currency).to eq('EUR') } + specify { expect(ex_entry.value_date).to be_kind_of(Date) } + specify { expect(ex_entry.value_date).to eq(Date.new(2013, 12, 27)) } + specify { expect(ex_entry.booking_date).to be_kind_of(Date) } + specify { expect(ex_entry.booking_date).to eq(Date.new(2013, 12, 27)) } + specify { expect(ex_entry.additional_information).to eq('Überweisungs-Gutschrift; GVC: SEPA Credit Transfer (Einzelbuchung-Haben)') } + specify { expect(ex_entry.description).to eq(ex_entry.additional_information) } + specify { expect(ex_entry.debit).to eq(true) } + specify { expect(ex_entry.debit?).to eq(ex_entry.debit) } + specify { expect(ex_entry.credit?).to eq(false) } + specify { expect(ex_entry.sign).to eq(-1) } + specify { expect(ex_entry.transactions).to all(be_kind_of(SepaFileParser::Transaction)) } + specify { expect(ex_entry.bank_reference).to eq('2013122710583450000') } + specify { expect(ex_entry.xml_data).to_not be_nil } + + context 'datetime' do + let(:camt) { SepaFileParser::File.parse('spec/fixtures/camt053/valid_example_with_datetime.xml') } + specify { expect(ex_entry.booking_datetime).to be_kind_of(DateTime) } + specify { expect(ex_entry.booking_datetime).to eq(DateTime.new(2023, 06, 21, 12, 35, 33.115965)) } + specify { expect(ex_entry.value_datetime).to eq(nil) } + end +end diff --git a/spec/lib/sepa_file_parser/general/group_header_spec.rb b/spec/lib/sepa_file_parser/general/group_header_spec.rb new file mode 100644 index 0000000..25e3572 --- /dev/null +++ b/spec/lib/sepa_file_parser/general/group_header_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe SepaFileParser::GroupHeader do + let(:camt) { SepaFileParser::File.parse 'spec/fixtures/camt053/valid_example.xml' } + let(:group_header) { camt.group_header } + + specify { expect(group_header).to be_kind_of(described_class) } + specify { expect(group_header.message_id).to eq("053D2013-12-27T22:05:03.0N130000005") } + specify { expect(group_header.creation_date_time).to be_kind_of(Time) } + specify { expect(group_header.message_pagination).to be_kind_of(SepaFileParser::MessagePagination) } + specify { expect(group_header.additional_information).to eq("") } + specify { expect(group_header.xml_data).to_not be_nil } +end + +RSpec.describe SepaFileParser::MessagePagination do + let(:camt) { SepaFileParser::File.parse 'spec/fixtures/camt053/valid_example.xml' } + let(:message_pagination) { camt.group_header.message_pagination } + + specify { expect(message_pagination).to be_kind_of(described_class) } + specify { expect(message_pagination.page_number).to eq(1) } + specify { expect(message_pagination.last_page?).to be_truthy } + specify { expect(message_pagination.xml_data).to_not be_nil } +end diff --git a/spec/lib/sepa_file_parser/general/postal_address_spec.rb b/spec/lib/sepa_file_parser/general/postal_address_spec.rb new file mode 100644 index 0000000..e1b18d0 --- /dev/null +++ b/spec/lib/sepa_file_parser/general/postal_address_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe SepaFileParser::PostalAddress do + let(:camt) { SepaFileParser::File.parse('spec/fixtures/camt053/valid_example.xml') } + let(:statements) { camt.statements } + let(:ex_stmt) { statements[0] } + let(:entries) { ex_stmt.entries } + let(:ex_entry) { entries[0] } + let(:transactions) { ex_entry.transactions } + let(:ex_transaction) { transactions[0] } + let(:address) { ex_transaction.postal_address } + + specify { expect(address.lines).to eq(["Berlin", "Infinite Loop 2", "12345"]) } + specify { expect(address.xml_data).to_not be_nil } + + context "version 8" do + let(:camt) { SepaFileParser::File.parse('spec/fixtures/camt053/valid_example_v8.xml') } + let(:ex_entry) { entries[2] } + + specify { expect(address.lines).to eq(["Hochstrasse 5", "4052 Basel"]) } + specify { expect(address.country).to eq("CH") } + specify { expect(address.street_name).to eq("") } + end + + context "with structured address" do + let(:camt) { SepaFileParser::File.parse('spec/fixtures/camt053/valid_example_v4.xml') } + let(:ex_entry) { entries[6] } + let(:ex_transaction) { transactions[1] } + let(:entity) { ex_transaction.debitor } + let(:address) { entity.postal_address } + + specify { expect(entity.name).to eq("Rutschmann Pia") } + specify { expect(address.street_name).to eq("Marktgasse") } + specify { expect(address.building_number).to eq("28") } + specify { expect(address.postal_code).to eq("9400") } + specify { expect(address.town_name).to eq("Rorschach") } + specify { expect(address.country).to eq("CH") } + end +end diff --git a/spec/lib/sepa_file_parser/general/record_spec.rb b/spec/lib/sepa_file_parser/general/record_spec.rb new file mode 100644 index 0000000..a70e15f --- /dev/null +++ b/spec/lib/sepa_file_parser/general/record_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe SepaFileParser::Record do + let(:camt) { SepaFileParser::File.parse('spec/fixtures/camt053/valid_example_v4.xml') } + let(:ex_entry) { camt.statements.first.entries[6] } + let(:ex_charges) { ex_entry.charges } + + specify { expect(ex_charges.records).to all(be_kind_of(described_class)) } + specify { expect(ex_charges.records[0].amount_in_cents).to be(540) } + specify { expect(ex_charges.records[0].amount).to eq(BigDecimal('5.40')) } + specify { expect(ex_charges.records[0].currency).to eq('CHF') } + specify { expect(ex_charges.records[0].type).to be_kind_of(SepaFileParser::Type::Proprietary) } + specify { expect(ex_charges.records[0].type.id).to eq('2') } + specify { expect(ex_charges.records[0].credit?).to be(false) } + specify { expect(ex_charges.records[0].charges_included?).to be(false) } + specify { expect(ex_charges.records[0].xml_data).to_not be_nil } + + specify { expect(ex_charges.records[1].amount_in_cents).to be(20) } + specify { expect(ex_charges.records[1].amount).to eq(BigDecimal('0.20')) } + specify { expect(ex_charges.records[1].currency).to eq('CHF') } + specify { expect(ex_charges.records[1].type).to be_kind_of(SepaFileParser::Type::Code) } + specify { expect(ex_charges.records[1].type.code).to eq('DISC') } + specify { expect(ex_charges.records[1].credit?).to be(true) } + specify { expect(ex_charges.records[1].charges_included?).to be(true) } + specify { expect(ex_charges.records[1].xml_data).to_not be_nil } +end diff --git a/spec/lib/sepa_file_parser/general/transaction_spec.rb b/spec/lib/sepa_file_parser/general/transaction_spec.rb new file mode 100644 index 0000000..f9ce397 --- /dev/null +++ b/spec/lib/sepa_file_parser/general/transaction_spec.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe SepaFileParser::Transaction do + context 'version 2' do + let(:camt) { SepaFileParser::File.parse('spec/fixtures/camt053/valid_example.xml') } + let(:statements) { camt.statements } + let(:ex_stmt) { statements[0] } + let(:entries) { ex_stmt.entries } + let(:ex_entry) { entries[0] } + let(:transactions) { ex_entry.transactions } + let(:ex_transaction) { transactions[0] } + + specify { expect(transactions).to all(be_kind_of(described_class)) } + + context '#amount' do + specify { expect(ex_transaction.amount).to be_kind_of(BigDecimal) } + specify { expect(ex_transaction.amount).to eq(BigDecimal('2')) } + specify { expect(ex_transaction.amount_in_cents).to eq(200) } + + context 'AmtDtls/InstdAmt' do + let(:camt) { SepaFileParser::File.parse('spec/fixtures/camt053/valid_example_with_instdamt.xml') } + specify { expect(ex_transaction.amount).to eq(BigDecimal('4500')) } + specify { expect(ex_transaction.amount_in_cents).to eq(450000) } + end + end + + context '#currency' do + specify { expect(ex_transaction.currency).to eq('EUR') } + + context 'AmtDtls/InstdAmt' do + let(:camt) { SepaFileParser::File.parse('spec/fixtures/camt053/valid_example_with_instdamt.xml') } + specify { expect(ex_transaction.currency).to eq('CHF') } + end + end + + specify { expect(ex_transaction.currency).to eq('EUR') } + specify { expect(ex_transaction.debit).to eq(true) } + specify { expect(ex_transaction.debit?).to eq(ex_transaction.debit) } + specify { expect(ex_transaction.credit?).to eq(false) } + specify { expect(ex_transaction.sign).to eq(-1) } + + specify { expect(ex_transaction.creditor).to be_kind_of(SepaFileParser::Creditor) } + specify { expect(ex_transaction.debitor).to be_kind_of(SepaFileParser::Debitor) } + specify { expect(ex_transaction.postal_address).to be_kind_of(SepaFileParser::PostalAddress) } + specify { expect(ex_transaction.remittance_information) + .to eq("TEST BERWEISUNG MITTELS BLZUND KONTONUMMER - DTA") } + specify { expect(ex_transaction.iban).to eq("DE09300606010012345671") } + specify { expect(ex_transaction.bic).to eq("DAAEDEDDXXX") } + specify { expect(ex_transaction.swift_code).to eq("NTRF") } + specify { expect(ex_transaction.reference).to eq("") } + specify { expect(ex_transaction.bank_reference).to eq("BankReference") } + specify { expect(ex_transaction.end_to_end_reference).to eq("EndToEndReference") } + specify { expect(ex_transaction.mandate_reference).to eq("MandateReference") } + specify { expect(ex_transaction.transaction_id).to eq("UniqueTransactionId") } + specify { expect(ex_transaction.creditor_identifier).to eq("CreditorIdentifier") } + specify { expect(ex_transaction.payment_information).to eq("PaymentIdentification") } + specify { + expect(ex_transaction.additional_information).to eq("AdditionalTransactionInformation") + } + specify { expect(ex_transaction.xml_data).to_not be_nil } + end + + context 'version 4' do + let(:camt) { SepaFileParser::File.parse('spec/fixtures/camt053/valid_example_v4.xml') } + let(:statements) { camt.statements } + let(:ex_stmt) { statements[0] } + let(:entries) { ex_stmt.entries } + let(:ex_entry) { entries[6] } + let(:transactions) { ex_entry.transactions } + let(:ex_transaction) { transactions[0] } + + context '#amount' do + specify { expect(ex_transaction.amount).to be_kind_of(BigDecimal) } + specify { expect(ex_transaction.amount).to eq(BigDecimal('100')) } + specify { expect(ex_transaction.amount_in_cents).to eq(10000) } + end + + context '#reason_code' do + let(:ex_entry) { entries[12] } + + specify { expect(ex_transaction.reason_code).to eq("MD06") } + end + + specify { expect(ex_transaction.name).to eq("Hans Kaufmann") } + specify { expect(ex_transaction.creditor_reference).to eq("CreditorReference") } + end + + context 'version 8' do + let(:camt) { SepaFileParser::File.parse('spec/fixtures/camt053/valid_example_v8.xml') } + let(:statements) { camt.statements } + let(:ex_stmt) { statements[0] } + let(:entries) { ex_stmt.entries } + let(:ex_entry) { entries[1] } + let(:transactions) { ex_entry.transactions } + let(:ex_transaction) { transactions[0] } + + context '#amount' do + specify { expect(ex_transaction.amount).to be_kind_of(BigDecimal) } + specify { expect(ex_transaction.amount).to eq(BigDecimal('88.85')) } + specify { expect(ex_transaction.amount_in_cents).to eq(8885) } + end + + + specify { expect(ex_transaction.name).to eq("Finanz AG") } + specify { expect(ex_transaction.creditor_reference).to eq("RF38000000000000000000552") } + specify { expect(ex_transaction.swift_code).to eq("A90") } + specify { expect(ex_transaction.bank_reference).to eq("0123171DO5126811") } + specify { expect(ex_transaction.end_to_end_reference).to eq("435A9287E088BDB1D97FAABD181C70C8") } + + context "#remittance_information" do + let(:ex_entry) { entries[0] } + let(:transactions) { ex_entry.transactions } + let(:ex_transaction) { transactions[0] } + + specify { expect(ex_transaction.remittance_information).to eq("INVOICE R77561") } + end + end +end diff --git a/spec/lib/sepa_file_parser/misc_spec.rb b/spec/lib/sepa_file_parser/misc_spec.rb new file mode 100644 index 0000000..4cc6672 --- /dev/null +++ b/spec/lib/sepa_file_parser/misc_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe SepaFileParser::Misc do + let(:dot_value) { "30.12" } + let(:comma_value) { "30,12" } + let(:integer_value) { "1" } + + context '#to_amount_in_cents' do + specify { expect(described_class.to_amount_in_cents(dot_value)).to be_kind_of(Integer) } + specify { expect(described_class.to_amount_in_cents(dot_value)).to eq(3012) } + specify { expect(described_class.to_amount_in_cents(comma_value)).to eq(3012) } + specify { expect(described_class.to_amount_in_cents(integer_value)).to eq(100) } + specify { expect(described_class.to_amount_in_cents('')).to eq(nil) } + specify { expect(described_class.to_amount_in_cents(nil)).to eq(nil) } + end + + context '#to_amount' do + specify { expect(described_class.to_amount(dot_value)).to be_kind_of(BigDecimal) } + specify { expect(described_class.to_amount(dot_value)).to eq(30.12) } + specify { expect(described_class.to_amount(comma_value)).to eq(30.12) } + specify { expect(described_class.to_amount(integer_value)).to eq(1.0) } + specify { expect(described_class.to_amount('')).to eq(nil) } + specify { expect(described_class.to_amount(nil)).to eq(nil) } + end +end diff --git a/spec/lib/sepa_file_parser/string_spec.rb b/spec/lib/sepa_file_parser/string_spec.rb new file mode 100644 index 0000000..7c3b0d3 --- /dev/null +++ b/spec/lib/sepa_file_parser/string_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe SepaFileParser::String do + context "parse" do + it "raises an exception if the namespace/format is unknown" do + expect{ + described_class.parse File.open('spec/fixtures/general/invalid_namespace.xml').read + }.to raise_exception(SepaFileParser::Errors::UnsupportedNamespaceError, 'urn:iso:std:iso:20022:tech:xsd:camt.053.001.03') + end + + it "does not raise an exception for a valid namespace" do + expect(SepaFileParser::Format052::Base).to receive(:new) + described_class.parse File.open('spec/fixtures/052/valid_namespace.xml').read + end + + it "does not raise an exception for a valid namespace" do + expect(SepaFileParser::Format053::Base).to receive(:new) + described_class.parse File.open('spec/fixtures/053/valid_namespace.xml').read + end + end +end diff --git a/spec/lib/sepa_file_parser/xml_spec.rb b/spec/lib/sepa_file_parser/xml_spec.rb new file mode 100644 index 0000000..194caf9 --- /dev/null +++ b/spec/lib/sepa_file_parser/xml_spec.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +require 'spec_helper' + +require 'securerandom' + +RSpec.describe SepaFileParser::Xml do + def parse_xml(file) + File.open file do |f| + Nokogiri::XML.parse f + end + end + + describe '.register' do + let(:namespace) { SecureRandom.uuid } # get a unique namespace per test + + context 'when parser is not supported' do + it 'raises SepaFileParser::Errors::NamespaceAlreadyRegistered' do + expect { + SepaFileParser::Xml.register(namespace, :unknown) + }.to raise_exception( + SepaFileParser::Errors::UnsupportedParserClass, + 'unknown' + ) + end + end + + SepaFileParser::Xml::PARSER_MAPPING.each do |parser_key, parser_class| + context "when the parser is #{parser_key}" do + context 'when the namespace is already registered' do + it 'raises SepaFileParser::Errors::NamespaceAlreadyRegistered' do + # should be ok + SepaFileParser::Xml.register(namespace, parser_key) + # raises an error + expect { + SepaFileParser::Xml.register(namespace, parser_key) + }.to raise_exception( + SepaFileParser::Errors::NamespaceAlreadyRegistered, + namespace + ) + end + end + + context 'when the namespace is not registered' do + it 'registers a parser for a namespace' do + SepaFileParser::Xml.register(namespace, parser_key) + + expect(SepaFileParser::Xml.instance_variable_get(:'@namespace_parsers')[namespace]) + .to eq(parser_class) + end + end + end + end + end + + describe '.parse' do + it "raises an exception if it is not an XML" do + expect { + described_class.parse 'not_xml' + }.to raise_exception(SepaFileParser::Errors::NotXMLError, String) + end + + it "raises an exception if the namespace/format is unknown" do + expect { + described_class.parse parse_xml('spec/fixtures/general/invalid_namespace.xml') + }.to raise_exception( + SepaFileParser::Errors::UnsupportedNamespaceError, + 'urn:iso:std:iso:20022:tech:xsd:camt.053.001.03' + ) + end + + it "does not raise an exception for a valid namespace" do + expect(SepaFileParser::Format052::Base).to receive(:new) + described_class.parse parse_xml('spec/fixtures/052/valid_namespace.xml') + end + + it "does not raise an exception for a valid namespace" do + expect(SepaFileParser::Format053::Base).to receive(:new) + described_class.parse parse_xml('spec/fixtures/053/valid_namespace.xml') + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..15a2bed --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,22 @@ +# encoding: utf-8 + +Dir[File.dirname(__FILE__) + '/../lib/*.rb'].each { |file| require file } +Dir["./spec/support/**/*.rb"].each { |file| require file } + +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + config.filter_run :focus + config.run_all_when_everything_filtered = true + + # This setting enables warnings. It's recommended, but in some cases may + # be too noisy due to issues in dependencies. + config.warnings = true + + config.disable_monkey_patching! +end