camt 0.0.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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