camt 0.0.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8bfc23e67cc63e70b660a029d859f138f397427a
4
- data.tar.gz: 3f1a661d0c80e14264b48198b3cb17bd953ec7ba
3
+ metadata.gz: fbf8cebe34c15ebcaee72f69f9d00fa5d42c7f71
4
+ data.tar.gz: 97458bfd5bbf68d50c1de444cf0bf3ce71261d3a
5
5
  SHA512:
6
- metadata.gz: e6208aa0f98688518eb4f6b5b80192088eebbe2129204f5cc834d6017f2814b296356092f94492c50682a8d6b822712974f9f1ef16c1b9f72214b2683445ed88
7
- data.tar.gz: 59d5e30cfbf93252e6f0b6996a0d0ffa429d6f068c776cd1334801f36a838f31b2125c997d85f8a0751c084afa143ce7014f89c39c6ddba7cec5ed3af2703e45
6
+ metadata.gz: 2040170ec53cd9887e563ccd81cfb45191a7ec27d78a8f7a0294175216e0ed9be9f591db7cfb0b3e8a9787d73fe91409c051b97098ec7cc33c5bb1736c06227a
7
+ data.tar.gz: 0dfebd812a609840381a6fe946188a746978630e0633365f8a388611e2c44951084fc6ce5cf5cbc0c8aa74a65d99e4fe8c9b5984ea3447b89958d5940527b889
data/.gitignore CHANGED
@@ -15,3 +15,4 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
+ .DS_Store
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
@@ -0,0 +1,2 @@
1
+ rvm:
2
+ - 2.1.5
data/Gemfile CHANGED
@@ -2,5 +2,3 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in camt.gemspec
4
4
  gemspec
5
-
6
- # gem 'debugger'
data/README.md CHANGED
@@ -2,18 +2,33 @@
2
2
 
3
3
  This gem is based on the Python implementation by Therp (https://code.launchpad.net/~therp-nl/banking-addons) (camt.py). It is still far from supporting the full CAMT.053 specification, but it does provide the most important transaction information.
4
4
 
5
+ [![Build Status](https://travis-ci.org/moiristo/camt.svg)](https://travis-ci.org/moiristo/camt)
6
+
5
7
  ## Installation
6
8
 
7
- Add to your Gemfile: gem 'camt', github: 'moiristo/camt'
9
+ Add to your Gemfile:
10
+
11
+ gem "camt"
12
+
13
+ ## Configuration
14
+
15
+ You need to set your default country code.
16
+
17
+ Camt.configure do |config|
18
+ config.default_country_code = "NL"
19
+ end
8
20
 
9
21
  ## Usage
10
22
 
11
- * Parse a CAMT file:
12
- * camt = Camt::File.parse 'camt.xml'
13
- * Get information:
14
- * puts camt.messages.inspect
15
- * puts camt.statements.inspect
16
- * puts camt.transactions.inspect
23
+ Parse a CAMT file:
24
+
25
+ camt = Camt::File.parse 'camt.xml'
26
+
27
+ Get information:
28
+
29
+ puts camt.messages.inspect
30
+ puts camt.statements.inspect
31
+ puts camt.transactions.inspect
17
32
 
18
33
  ## TODO
19
34
 
data/Rakefile CHANGED
@@ -1,10 +1,8 @@
1
1
  require "bundler/gem_tasks"
2
- require 'rake/testtask'
3
2
 
4
- Rake::TestTask.new(:test) do |test|
5
- test.libs << 'lib' << 'test'
6
- test.pattern = 'test/**/*_test.rb'
7
- test.verbose = true
8
- end
3
+ task :default => [:spec]
9
4
 
10
- task :default => :test
5
+ desc 'run Rspec specs'
6
+ task :spec do
7
+ sh 'rspec spec'
8
+ end
@@ -19,7 +19,9 @@ Gem::Specification.new do |spec|
19
19
  spec.require_paths = ["lib"]
20
20
 
21
21
  spec.add_dependency "nokogiri"
22
+ spec.add_dependency "activesupport"
22
23
 
23
24
  spec.add_development_dependency "bundler", "~> 1.3"
24
25
  spec.add_development_dependency "rake"
26
+ spec.add_development_dependency "rspec"
25
27
  end
@@ -1,10 +1,17 @@
1
1
  require 'nokogiri'
2
2
  require 'time'
3
+ require 'bigdecimal'
4
+
5
+ require 'active_support/core_ext/object/try'
6
+ require 'active_support/core_ext/module/delegation'
7
+ require 'active_support/configurable'
3
8
 
4
9
  require 'camt/version'
5
- require 'camt/object_extension'
6
10
  require 'camt/file'
7
11
  require 'camt/parser'
12
+ require 'camt/amount'
13
+ require 'camt/statement'
14
+ require 'camt/transaction'
8
15
 
9
16
  module Camt
10
17
 
@@ -13,26 +20,6 @@ module Camt
13
20
  Message = Struct.new(:group_header, :statements)
14
21
  GroupHeader = Struct.new(:message_id, :created_at, :recipient, :pagination, :additional_info)
15
22
 
16
- Statement = Struct.new(
17
- :id,
18
- :date,
19
- :created_at,
20
- :local_account,
21
- :local_currency,
22
- :start_balance,
23
- :end_balance,
24
- :electronic_sequence_number,
25
- :transactions
26
- )
27
-
28
- Transaction = Struct.new(
29
- :execution_date,
30
- :effective_date,
31
- :transfer_type,
32
- :transferred_amount,
33
- :transaction_details
34
- )
35
-
36
23
  Reasons = {
37
24
  'EN' => {
38
25
  'AC01' => "Account identifier incorrect (i.e. invalid IBAN)",
@@ -97,4 +84,10 @@ module Camt
97
84
  }
98
85
  }
99
86
 
87
+ include ActiveSupport::Configurable
88
+
89
+ def self.configure
90
+ yield config
91
+ end
92
+
100
93
  end
@@ -0,0 +1,22 @@
1
+ module Camt
2
+ class Amount
3
+
4
+ attr_reader :node
5
+
6
+ def initialize(xml_node)
7
+ @node = xml_node
8
+ end
9
+
10
+ def value
11
+ sign * BigDecimal.new(node.at('./Amt').text)
12
+ end
13
+
14
+ def sign
15
+ node.at('./CdtDbtInd').text == 'DBIT' ? -1 : 1
16
+ end
17
+
18
+ def currency
19
+ node.at('./Amt').attribute('Ccy').value
20
+ end
21
+ end
22
+ end
@@ -3,21 +3,26 @@ module Camt
3
3
  attr_accessor :code, :country_code, :name
4
4
  attr_accessor :doc, :ns
5
5
 
6
+ delegate :errors, :to => :doc
7
+
6
8
  def self.parse file
7
- Camt::File.new Nokogiri::XML ::File.read(file)
9
+ Camt::File.new(Nokogiri::XML(::File.read(file)))
8
10
  end
9
11
 
10
- def initialize doc, options = { code: 'CAMT', country_code: 'NL', name: 'Generic CAMT Format' }
12
+ def initialize(doc, options = {})
11
13
  self.code = options[:code] || 'CAMT'
12
- self.country_code = options[:country_code] || 'NL'
14
+ self.country_code = options[:country_code] || Camt.config.default_country_code || raise(ArgumentError.new("No country_code given and no default_country_code configured."))
13
15
  self.name = options[:name] || 'Generic CAMT Format'
14
16
 
15
17
  self.doc = doc
16
18
  self.ns = doc.namespaces['xmlns']
17
19
 
18
- check_version
20
+ check_version if valid?
19
21
  end
20
22
 
23
+ def valid?
24
+ doc.errors.empty?
25
+ end
21
26
 
22
27
  def check_version
23
28
  # Sanity check the document's namespace
@@ -44,4 +49,4 @@ module Camt
44
49
  end
45
50
 
46
51
  end
47
- end
52
+ end
@@ -3,136 +3,13 @@ module Camt
3
3
 
4
4
  attr_accessor :file
5
5
 
6
- def get_balance_type_node node, balance_type
7
- # :param node: BkToCstmrStmt/Stmt/Bal node
8
- # :param balance type: one of 'OPBD', 'PRCD', 'ITBD', 'CLBD'
9
- return node.at("./Bal/Tp/CdOrPrtry/Cd[text()='#{balance_type}']/../../..")
10
- end
11
-
12
- def parse_amount(node)
13
- # Parse an element that contains both Amount and CreditDebitIndicator
14
- #
15
- # :return: signed amount
16
- # :returntype: float
17
-
18
- sign = node.at('./CdtDbtInd').text == 'DBIT' ? -1 : 1
19
- return sign * node.at('./Amt').text.to_f
20
- end
21
-
22
- def get_start_balance(node)
23
- # Find the (only) balance node with code OpeningBalance, or
24
- # the only one with code 'PreviousClosingBalance'
25
- # or the first balance node with code InterimBalance in
26
- # the case of preceeding pagination.
27
- #
28
- # :param node: BkToCstmrStmt/Stmt/Bal node
29
- balance_type_node = nil
30
- ['OPBD', 'PRCD', 'ITBD'].detect{|code| balance_type_node = get_balance_type_node(node, code) }
31
- parse_amount balance_type_node
32
- end
33
-
34
- def get_end_balance(node)
35
- # Find the (only) balance node with code ClosingBalance, or
36
- # the second (and last) balance node with code InterimBalance in
37
- # the case of continued pagination.
38
- #
39
- # :param node: BkToCstmrStmt/Stmt/Bal node
40
- balance_type_node = nil
41
- ['CLBD', 'ITBD'].detect{|code| balance_type_node = get_balance_type_node(node, code) }
42
- parse_amount balance_type_node
43
- end
44
-
45
- def parse_Stmt node
6
+ def parse_Stmt(node)
46
7
  # Parse a single Stmt node.
47
8
  #
48
9
  # Be sure to craft a unique, but short enough statement identifier,
49
10
  # as it is used as the basis of the generated move lines' names
50
11
  # which overflow when using the full IBAN and CAMT statement id.
51
-
52
- statement = Statement.new
53
-
54
- statement.id = node.at('./Id').text
55
- statement.electronic_sequence_number = node.at('./ElctrncSeqNb').text
56
- statement.created_at = Time.parse(node.at('./CreDtTm').text)
57
- statement.local_account = node.at('./Acct/Id').text.strip
58
- statement.local_currency = node.at('./Acct/Ccy').text
59
- statement.date = Time.parse(node.at('./Ntry[1]/ValDt/Dt | ./Ntry[1]/ValDt/DtTm').text)
60
- statement.start_balance = get_start_balance(node)
61
- statement.end_balance = get_end_balance(node)
62
- statement.transactions = node.xpath('./Ntry').map{ |node| parse_Ntry(node) }
63
-
64
- statement
65
- end
66
-
67
- def get_transfer_type node
68
- # Map properietary codes from BkTxCd/Prtry/Cd.
69
- # :param node: BkTxCd/Prtry node
70
- { proprietary_code: node.at('./Cd').text, proprietary_issuer: node.at('./Issr').text } if node
71
- end
72
-
73
- def parse_Ntry node
74
- # :param node: Ntry node
75
-
76
- transaction = Transaction.new
77
- transaction.execution_date = node.at('./BookgDt/Dt | ./BookgDt/DtTm').text
78
- transaction.effective_date = node.at('./ValDt/Dt | ./ValDt/DtTm').text
79
- transaction.transfer_type = get_transfer_type(node.at('./BkTxCd/Prtry'))
80
- transaction.transferred_amount = parse_amount(node)
81
- transaction.transaction_details = node.xpath('.//NtryDtls//TxDtls').map{ |node| parse_TxDtls(node) }
82
-
83
- transaction
84
- end
85
-
86
- def get_party_values node
87
- # Determine to get either the debtor or creditor party node
88
- # and extract the available data from it
89
- values = {}
90
-
91
- party_type = node.at('../../CdtDbtInd').text == 'CRDT' ? 'Dbtr' : 'Cdtr'
92
-
93
- party_node = node.at("./RltdPties/#{party_type}")
94
- account_node = node.at("./RltdPties/#{party_type}Acct/Id")
95
- bic_node = node.at("./RltdAgts/#{party_type}Agt/FinInstnId/BIC")
96
-
97
- if party_node
98
- values[:remote_owner] = party_node.at('./Nm').try(:text)
99
- values[:remote_owner_country] = party_node.at('./PstlAdr/Ctry').try(:text)
100
- values[:remote_owner_address] = party_node.at('./PstlAdr/AdrLine').try(:text)
101
- end
102
-
103
- if account_node
104
- values[:remote_account] = account_node.at('./IBAN').try(:text)
105
- values[:remote_account] ||= account_node.at('./Othr/Id').try(:text)
106
- values[:remote_bank_bic] = bic_node.text if bic_node
107
- end
108
-
109
- return values
110
- end
111
-
112
- def parse_TxDtls node
113
- # Parse a single TxDtls node
114
- transaction_details = {}
115
-
116
- if (unstructured = node.xpath('./RmtInf/Ustrd')).any?
117
- transaction_details[:messages] = unstructured.map(&:text)
118
- end
119
-
120
- if (structured = node.xpath('./RmtInf/Strd/CdtrRefInf/Ref | ./Refs/EndToEndId')).any?
121
- transaction_details[:references] = structured.map(&:text)
122
- end
123
-
124
- if mandate_identifier = node.at('./Refs/MndtId').try(:text)
125
- transaction_details[:mandate_identifier] = mandate_identifier
126
- end
127
-
128
- if reason = node.at('./RtrInf/Rsn/Cd').try(:text)
129
- reason_language = Camt::Reasons.keys.include?(file.country_code) ? file.country_code : 'EN'
130
- transaction_details[:reason] = { code: reason, description: Camt::Reasons[reason_language][reason] }
131
- end
132
-
133
- transaction_details[:party] = get_party_values node
134
-
135
- transaction_details
12
+ Statement.new(node, file.country_code)
136
13
  end
137
14
 
138
15
  def parse_message node
@@ -171,4 +48,4 @@ module Camt
171
48
  end
172
49
 
173
50
  end
174
- end
51
+ end
@@ -0,0 +1,86 @@
1
+ module Camt
2
+ class Statement
3
+
4
+ attr_reader :node, :country_code
5
+
6
+ def initialize(xml_node, country_code)
7
+ @node = xml_node
8
+ @country_code = country_code
9
+ end
10
+
11
+ def id
12
+ @id ||= node.at('./Id').text
13
+ end
14
+
15
+ def electronic_sequence_number
16
+ @electronic_sequence_number ||= node.at('./ElctrncSeqNb').text
17
+ end
18
+
19
+ def created_at
20
+ @created_at ||= Time.parse(node.at('./CreDtTm').text)
21
+ end
22
+
23
+ def local_account
24
+ @local_account ||= node.at('./Acct/Id').text.strip
25
+ end
26
+
27
+ alias_method :iban, :local_account
28
+
29
+ def local_currency
30
+ @local_currency ||= node.at('./Acct/Ccy').try(:text) ||
31
+ node.at('./Bal/Amt').attribute("Ccy").value
32
+ end
33
+
34
+ def date
35
+ @date ||= Date.parse(node.at('./Ntry[1]/ValDt/Dt | ./Ntry[1]/ValDt/DtTm').text)
36
+ end
37
+
38
+ def start_balance
39
+ @start_balance ||= get_start_balance
40
+ end
41
+
42
+ def end_balance
43
+ @end_balance ||= get_end_balance
44
+ end
45
+
46
+ def transactions
47
+ @transactions ||= node.xpath('./Ntry').map { |node| parse_Ntry(node) }
48
+ end
49
+
50
+ private
51
+
52
+ def get_balance_type_node node, balance_type
53
+ # :param node: BkToCstmrStmt/Stmt/Bal node
54
+ # :param balance type: one of 'OPBD', 'PRCD', 'ITBD', 'CLBD'
55
+ node.at("./Bal/Tp/CdOrPrtry/Cd[text()='#{balance_type}']/../../..")
56
+ end
57
+
58
+ def get_start_balance
59
+ # Find the (only) balance node with code OpeningBalance, or
60
+ # the only one with code 'PreviousClosingBalance'
61
+ # or the first balance node with code InterimBalance in
62
+ # the case of preceeding pagination.
63
+ #
64
+ # :param node: BkToCstmrStmt/Stmt/Bal node
65
+ balance_type_node = nil
66
+ ['OPBD', 'PRCD', 'ITBD'].detect{|code| balance_type_node = get_balance_type_node(node, code) }
67
+ Amount.new(balance_type_node).value
68
+ end
69
+
70
+ def get_end_balance
71
+ # Find the (only) balance node with code ClosingBalance (Closing Booked), or
72
+ # the second (and last) balance node with code InterimBalance (Interim Booked) in
73
+ # the case of continued pagination. Use ClosingAvailable otherwise.
74
+ #
75
+ # :param node: BkToCstmrStmt/Stmt/Bal node
76
+ balance_type_node = nil
77
+ ['CLBD', 'ITBD', 'CLAV'].detect{|code| balance_type_node = get_balance_type_node(node, code) }
78
+ Amount.new(balance_type_node).value if balance_type_node
79
+ end
80
+
81
+ def parse_Ntry(node)
82
+ # :param node: Ntry node
83
+ Transaction.new(node, country_code)
84
+ end
85
+ end
86
+ end