sepa_file_parser 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/lib/sepa_file_parser/camt052/base.rb +18 -0
  3. data/lib/sepa_file_parser/camt052/report.rb +73 -0
  4. data/lib/sepa_file_parser/camt053/base.rb +18 -0
  5. data/lib/sepa_file_parser/camt053/statement.rb +75 -0
  6. data/lib/sepa_file_parser/camt054/base.rb +18 -0
  7. data/lib/sepa_file_parser/camt054/notification.rb +54 -0
  8. data/lib/sepa_file_parser/errors.rb +12 -0
  9. data/lib/sepa_file_parser/file.rb +10 -0
  10. data/lib/sepa_file_parser/general/account.rb +66 -0
  11. data/lib/sepa_file_parser/general/account_balance.rb +61 -0
  12. data/lib/sepa_file_parser/general/batch_detail.rb +24 -0
  13. data/lib/sepa_file_parser/general/charges.rb +25 -0
  14. data/lib/sepa_file_parser/general/creditor.rb +46 -0
  15. data/lib/sepa_file_parser/general/debitor.rb +46 -0
  16. data/lib/sepa_file_parser/general/entry.rb +117 -0
  17. data/lib/sepa_file_parser/general/group_header.rb +38 -0
  18. data/lib/sepa_file_parser/general/pain_entry.rb +46 -0
  19. data/lib/sepa_file_parser/general/postal_address.rb +45 -0
  20. data/lib/sepa_file_parser/general/record.rb +49 -0
  21. data/lib/sepa_file_parser/general/transaction.rb +139 -0
  22. data/lib/sepa_file_parser/general/type/builder.rb +20 -0
  23. data/lib/sepa_file_parser/general/type/code.rb +13 -0
  24. data/lib/sepa_file_parser/general/type/proprietary.rb +13 -0
  25. data/lib/sepa_file_parser/misc.rb +28 -0
  26. data/lib/sepa_file_parser/pain001/base.rb +18 -0
  27. data/lib/sepa_file_parser/pain001/payment_information.rb +44 -0
  28. data/lib/sepa_file_parser/pain008/base.rb +18 -0
  29. data/lib/sepa_file_parser/pain008/payment_information.rb +44 -0
  30. data/lib/sepa_file_parser/register.rb +23 -0
  31. data/lib/sepa_file_parser/string.rb +10 -0
  32. data/lib/sepa_file_parser/version.rb +5 -0
  33. data/lib/sepa_file_parser/xml.rb +45 -0
  34. data/lib/sepa_file_parser.rb +38 -0
  35. metadata +160 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5233f32c897d7e33ce7a00b1213f4933fed2c6d9d3c47d2c4367268e21f5dccd
4
+ data.tar.gz: 5562ca231c07df7848143dfa5f0d7b1dde9fc81ebf866ad18eb55b346596bf04
5
+ SHA512:
6
+ metadata.gz: b151cf8b849f21e33e01d4c2b85268c150cf010ab0c62a5743dd9ef004ebac010b5f46f237d6b9c3ad5764aa26ca6eeda27a1bb6fe45349fa810259e560baa2f
7
+ data.tar.gz: b7557e088a656bc18f892f008e7aba228efbdc60a0da2b998735c9db0d7935b9babecbf25679618d60683f64cf1a318807c0c3045c3abf1804eb908fabc423ab
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SepaFileParser
4
+ module Camt052
5
+ class Base
6
+ attr_reader :group_header, :reports, :xml_data
7
+
8
+ def initialize(xml_data)
9
+ @xml_data = xml_data
10
+ # BkToCstmrAccptRpt = Bank to Customer Account Report
11
+ grphdr = xml_data.xpath('BkToCstmrAcctRpt/GrpHdr')
12
+ @group_header = SepaFileParser::GroupHeader.new(grphdr)
13
+ reports = xml_data.xpath('BkToCstmrAcctRpt/Rpt')
14
+ @reports = reports.map{ |x| Report.new(x) }
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SepaFileParser
4
+ module Camt052
5
+ class Report
6
+
7
+ attr_reader :xml_data
8
+
9
+ def initialize(xml_data)
10
+ @xml_data = xml_data
11
+ end
12
+
13
+ def identification
14
+ @identification ||= xml_data.xpath('Id/text()').text
15
+ end
16
+
17
+ def generation_date
18
+ @generation_date ||= Time.parse(xml_data.xpath('CreDtTm/text()').text)
19
+ end
20
+
21
+ def account
22
+ @account ||= SepaFileParser::Account.from_camt_data(xml_data.xpath('Acct').first)
23
+ end
24
+
25
+ def entries
26
+ @entries ||= xml_data.xpath('Ntry').map{ |x| SepaFileParser::Entry.new(x) }
27
+ end
28
+
29
+ def legal_sequence_number
30
+ @legal_sequence_number ||= xml_data.xpath('LglSeqNb/text()').text
31
+ end
32
+
33
+ def from_date_time
34
+ @from_date_time ||= (x = xml_data.xpath('FrToDt/FrDtTm')).empty? ? nil : Time.parse(x.first.content)
35
+ end
36
+
37
+ def to_date_time
38
+ @to_date_time ||= (x = xml_data.xpath('FrToDt/ToDtTm')).empty? ? nil : Time.parse(x.first.content)
39
+ end
40
+
41
+ def opening_balance
42
+ @opening_balance ||= begin
43
+ bal = xml_data.xpath('Bal/Tp//Cd[contains(text(), "PRCD")]').first.ancestors('Bal')
44
+ date = bal.xpath('Dt/Dt/text()').text
45
+ credit = bal.xpath('CdtDbtInd/text()').text == 'CRDT'
46
+ currency = bal.xpath('Amt').attribute('Ccy').value
47
+ SepaFileParser::AccountBalance.new bal.xpath('Amt/text()').text, currency, date, credit
48
+ end
49
+ end
50
+ alias_method :opening_or_intermediary_balance, :opening_balance
51
+
52
+ def closing_balance
53
+ @closing_balance ||= begin
54
+ bal = xml_data.xpath('Bal/Tp//Cd[contains(text(), "CLBD")]').first.ancestors('Bal')
55
+ date = bal.xpath('Dt/Dt/text()').text
56
+ credit = bal.xpath('CdtDbtInd/text()').text == 'CRDT'
57
+ currency = bal.xpath('Amt').attribute('Ccy').value
58
+ SepaFileParser::AccountBalance.new bal.xpath('Amt/text()').text, currency, date, credit
59
+ end
60
+ end
61
+ alias_method :closing_or_intermediary_balance, :closing_balance
62
+
63
+
64
+ def source
65
+ xml_data.to_s
66
+ end
67
+
68
+ def self.parse(xml)
69
+ self.new Nokogiri::XML(xml).xpath('Report')
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SepaFileParser
4
+ module Camt053
5
+ class Base
6
+ attr_reader :group_header, :statements, :xml_data
7
+
8
+ def initialize(xml_data)
9
+ @xml_data = xml_data
10
+
11
+ grphdr = xml_data.xpath('BkToCstmrStmt/GrpHdr')
12
+ @group_header = GroupHeader.new(grphdr)
13
+ statements = xml_data.xpath('BkToCstmrStmt/Stmt')
14
+ @statements = statements.map{ |x| Statement.new(x) }
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SepaFileParser
4
+ module Camt053
5
+ class Statement
6
+
7
+ attr_reader :xml_data
8
+
9
+ def initialize(xml_data)
10
+ @xml_data = xml_data
11
+ end
12
+
13
+ def identification
14
+ @identification ||= xml_data.xpath('Id/text()').text
15
+ end
16
+
17
+ def generation_date
18
+ @generation_date ||= Time.parse(xml_data.xpath('CreDtTm/text()').text)
19
+ end
20
+
21
+ def from_date_time
22
+ @from_date_time ||= (x = xml_data.xpath('FrToDt/FrDtTm')).empty? ? nil : Time.parse(x.first.content)
23
+ end
24
+
25
+ def to_date_time
26
+ @to_date_time ||= (x = xml_data.xpath('FrToDt/ToDtTm')).empty? ? nil : Time.parse(x.first.content)
27
+ end
28
+
29
+ def account
30
+ @account ||= SepaFileParser::Account.from_camt_data(xml_data.xpath('Acct').first)
31
+ end
32
+
33
+ def entries
34
+ @entries ||= xml_data.xpath('Ntry').map{ |x| Entry.new(x) }
35
+ end
36
+
37
+ def legal_sequence_number
38
+ @legal_sequence_number ||= xml_data.xpath('LglSeqNb/text()').text
39
+ end
40
+
41
+ def electronic_sequence_number
42
+ @electronic_sequence_number ||= xml_data.xpath('ElctrncSeqNb/text()').text
43
+ end
44
+
45
+ def opening_balance
46
+ @opening_balance ||= begin
47
+ bal = xml_data.xpath('Bal/Tp//Cd[contains(text(), "OPBD") or contains(text(), "PRCD")]').first.ancestors('Bal')
48
+ date = bal.xpath('Dt/Dt/text()').text
49
+ credit = bal.xpath('CdtDbtInd/text()').text == 'CRDT'
50
+ currency = bal.xpath('Amt').attribute('Ccy').value
51
+ AccountBalance.new bal.xpath('Amt/text()').text, currency, date, credit
52
+ end
53
+ end
54
+
55
+ def closing_balance
56
+ @closing_balance ||= begin
57
+ bal = xml_data.xpath('Bal/Tp//Cd[contains(text(), "CLBD")]').first.ancestors('Bal')
58
+ date = bal.xpath('Dt/Dt/text()').text
59
+ credit = bal.xpath('CdtDbtInd/text()').text == 'CRDT'
60
+ currency = bal.xpath('Amt').attribute('Ccy').value
61
+ AccountBalance.new bal.xpath('Amt/text()').text, currency, date, credit
62
+ end
63
+ end
64
+ alias_method :closing_or_intermediary_balance, :closing_balance
65
+
66
+ def source
67
+ xml_data.to_s
68
+ end
69
+
70
+ def self.parse(xml)
71
+ self.new Nokogiri::XML(xml).xpath('Stmt')
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SepaFileParser
4
+ module Camt054
5
+ class Base
6
+ attr_reader :group_header, :notifications, :xml_data
7
+
8
+ def initialize(xml_data)
9
+ @xml_data = xml_data
10
+
11
+ grphdr = xml_data.xpath('BkToCstmrDbtCdtNtfctn/GrpHdr')
12
+ @group_header = GroupHeader.new(grphdr)
13
+ notifications = xml_data.xpath('BkToCstmrDbtCdtNtfctn/Ntfctn')
14
+ @notifications = notifications.map{ |x| Notification.new(x) }
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SepaFileParser
4
+ module Camt054
5
+ class Notification
6
+
7
+ attr_reader :xml_data
8
+
9
+ def initialize(xml_data)
10
+ @xml_data = xml_data
11
+ end
12
+
13
+ # @return [String]
14
+ def identification
15
+ @identification ||= xml_data.xpath('Id/text()').text
16
+ end
17
+
18
+ # @return [Time]
19
+ def generation_date
20
+ @generation_date ||= Time.parse(xml_data.xpath('CreDtTm/text()').text)
21
+ end
22
+
23
+ # @return [Time, nil]
24
+ def from_date_time
25
+ @from_date_time ||= (x = xml_data.xpath('FrToDt/FrDtTm')).empty? ? nil : Time.parse(x.first.content)
26
+ end
27
+
28
+ # @return [Time, nil]
29
+ def to_date_time
30
+ @to_date_time ||= (x = xml_data.xpath('FrToDt/ToDtTm')).empty? ? nil : Time.parse(x.first.content)
31
+ end
32
+
33
+ # @return [Account]
34
+ def account
35
+ @account ||= SepaFileParser::Account.from_camt_data(xml_data.xpath('Acct').first)
36
+ end
37
+
38
+ # @return [Array<Entry>]
39
+ def entries
40
+ @entries ||= xml_data.xpath('Ntry').map{ |x| Entry.new(x) }
41
+ end
42
+
43
+ # @return [String]
44
+ def source
45
+ xml_data.to_s
46
+ end
47
+
48
+ # @return [SepaFileParser::Camt054::Notification]
49
+ def self.parse(xml)
50
+ self.new Nokogiri::XML(xml).xpath('Ntfctn')
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SepaFileParser
4
+ module Errors
5
+ class BaseError < StandardError; end
6
+
7
+ class NamespaceAlreadyRegistered < BaseError; end
8
+ class NotXMLError < BaseError; end
9
+ class UnsupportedParserClass < BaseError; end
10
+ class UnsupportedNamespaceError < BaseError; end
11
+ end
12
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SepaFileParser
4
+ class File
5
+ def self.parse(path)
6
+ data = ::File.read(path)
7
+ SepaFileParser::String.parse(data)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SepaFileParser
4
+ class Account
5
+
6
+ def self.from_camt_data(xml_data)
7
+ self.new(
8
+ iban: xml_data.xpath('Id/IBAN/text()').text,
9
+ other_id: xml_data.xpath('Id/Othr/Id/text()').text,
10
+ bic: [
11
+ xml_data.xpath('Svcr/FinInstnId/BIC/text()').text,
12
+ xml_data.xpath('Svcr/FinInstnId/BICFI/text()').text,
13
+ ].reject(&:empty?).first.to_s,
14
+ bank_name: xml_data.xpath('Svcr/FinInstnId/Nm/text()').text,
15
+ currency: xml_data.xpath('Ccy/text()').text,
16
+ )
17
+ end
18
+
19
+ def self.from_pain_data(xml_data, currency, context)
20
+ self.new(
21
+ iban: xml_data.xpath("#{context}Acct/Id/IBAN/text()").text,
22
+ bic: xml_data.xpath("#{context}Agt/FinInstnId/BIC/text()").text,
23
+ bank_name: xml_data.xpath("#{context}/Nm/text()").text,
24
+ currency: currency,
25
+ )
26
+ end
27
+
28
+ def initialize(iban: nil, other_id: nil, bic:, bank_name:, currency:)
29
+ @iban = iban
30
+ @other_id = other_id
31
+ @bic = bic
32
+ @bank_name = bank_name
33
+ @currency = currency
34
+ end
35
+
36
+ # @return [String]
37
+ def iban
38
+ @iban.to_s
39
+ end
40
+
41
+ # @return [String]
42
+ def other_id
43
+ @other_id.to_s
44
+ end
45
+
46
+ # @return [String]
47
+ def account_number
48
+ iban != '' ? iban : other_id
49
+ end
50
+
51
+ # @return [String]
52
+ def bic
53
+ @bic.to_s
54
+ end
55
+
56
+ # @return [String]
57
+ def bank_name
58
+ @bank_name.to_s
59
+ end
60
+
61
+ # @return [String]
62
+ def currency
63
+ @currency.to_s
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SepaFileParser
4
+ class AccountBalance
5
+
6
+ # @param amount [String]
7
+ # @param currency [String]
8
+ # @param date [String]
9
+ # @param credit [Boolean]
10
+ def initialize(amount, currency, date, credit = false)
11
+ @amount = amount
12
+ @currency = currency
13
+ @date = date
14
+ @credit = credit
15
+ end
16
+
17
+ # @return [String]
18
+ def currency
19
+ @currency
20
+ end
21
+
22
+ # @return [Date]
23
+ def date
24
+ Date.parse @date
25
+ end
26
+
27
+ # @return [Integer] either 1 or -1
28
+ def sign
29
+ credit? ? 1 : -1
30
+ end
31
+
32
+ # @return [Boolean]
33
+ def credit?
34
+ @credit
35
+ end
36
+
37
+ # @return [BigDecimal]
38
+ def amount
39
+ SepaFileParser::Misc.to_amount(@amount)
40
+ end
41
+
42
+ # @return [Integer]
43
+ def amount_in_cents
44
+ SepaFileParser::Misc.to_amount_in_cents(@amount)
45
+ end
46
+
47
+ # @return [BigDecimal]
48
+ def signed_amount
49
+ amount * sign
50
+ end
51
+
52
+ # @return [Hash{String => BigDecimal, Integer}]
53
+ def to_h
54
+ {
55
+ 'amount' => amount,
56
+ 'amount_in_cents' => amount_in_cents,
57
+ 'sign' => sign
58
+ }
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SepaFileParser
4
+ class BatchDetail
5
+
6
+ attr_reader :xml_data
7
+
8
+ def initialize(xml_data)
9
+ @xml_data = xml_data
10
+ end
11
+
12
+ def payment_information_identification
13
+ @payment_information_identification ||= xml_data.xpath('PmtInfId/text()').text
14
+ end
15
+
16
+ def msg_id # may be missing
17
+ @msg_id ||= xml_data.xpath('MsgId/text()').text
18
+ end
19
+
20
+ def number_of_transactions
21
+ @number_of_transactions ||= xml_data.xpath('NbOfTxs/text()').text
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SepaFileParser
4
+ class Charges
5
+
6
+ attr_reader :xml_data
7
+
8
+ def initialize(xml_data)
9
+ @xml_data = xml_data
10
+ @total_charges_and_tax_amount = xml_data.xpath('TtlChrgsAndTaxAmt/text()').text
11
+ end
12
+
13
+ def total_charges_and_tax_amount
14
+ SepaFileParser::Misc.to_amount(@total_charges_and_tax_amount)
15
+ end
16
+
17
+ def total_charges_and_tax_amount_in_cents
18
+ SepaFileParser::Misc.to_amount_in_cents(@total_charges_and_tax_amount)
19
+ end
20
+
21
+ def records
22
+ @records ||= xml_data.xpath('Rcrd').map{ |x| SepaFileParser::Record.new(x) }
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SepaFileParser
4
+ class Creditor
5
+
6
+ attr_reader :xml_data
7
+
8
+ def initialize(xml_data)
9
+ @xml_data = xml_data
10
+ end
11
+
12
+ def name
13
+ @name ||= [
14
+ xml_data.xpath('RltdPties/Cdtr/Nm/text()').text,
15
+ xml_data.xpath('RltdPties/Cdtr/Pty/Nm/text()').text,
16
+ ].reject(&:empty?).first.to_s
17
+ end
18
+
19
+ def iban
20
+ @iban ||= xml_data.xpath('RltdPties/CdtrAcct/Id/IBAN/text()').text
21
+ end
22
+
23
+ def bic
24
+ @bic ||= [
25
+ xml_data.xpath('RltdAgts/CdtrAgt/FinInstnId/BIC/text()').text,
26
+ xml_data.xpath('RltdAgts/CdtrAgt/FinInstnId/BICFI/text()').text,
27
+ ].reject(&:empty?).first.to_s
28
+ end
29
+
30
+ def bank_name
31
+ @bank_name ||= xml_data.xpath('RltdAgts/CdtrAgt/FinInstnId/Nm/text()').text
32
+ end
33
+
34
+ # @return [SepaFileParser::PostalAddress, nil]
35
+ def postal_address # May be missing
36
+ postal_address = [
37
+ xml_data.xpath('RltdPties/Cdtr/PstlAdr'),
38
+ xml_data.xpath('RltdPties/Cdtr/Pty/PstlAdr'),
39
+ ].reject(&:empty?).first
40
+
41
+ return nil if postal_address == nil || postal_address.empty?
42
+
43
+ @address ||= SepaFileParser::PostalAddress.new(postal_address)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SepaFileParser
4
+ class Debitor
5
+
6
+ attr_reader :xml_data
7
+
8
+ def initialize(xml_data)
9
+ @xml_data = xml_data
10
+ end
11
+
12
+ def name
13
+ @name ||= [
14
+ xml_data.xpath('RltdPties/Dbtr/Nm/text()').text,
15
+ xml_data.xpath('RltdPties/Dbtr/Pty/Nm/text()').text,
16
+ ].reject(&:empty?).first.to_s
17
+ end
18
+
19
+ def iban
20
+ @iban ||= xml_data.xpath('RltdPties/DbtrAcct/Id/IBAN/text()').text
21
+ end
22
+
23
+ def bic
24
+ @bic ||= [
25
+ xml_data.xpath('RltdAgts/DbtrAgt/FinInstnId/BIC/text()').text,
26
+ xml_data.xpath('RltdAgts/DbtrAgt/FinInstnId/BICFI/text()').text,
27
+ ].reject(&:empty?).first.to_s
28
+ end
29
+
30
+ def bank_name
31
+ @bank_name ||= xml_data.xpath('RltdAgts/DbtrAgt/FinInstnId/Nm/text()').text
32
+ end
33
+
34
+ # @return [SepaFileParser::PostalAddress, nil]
35
+ def postal_address # May be missing
36
+ postal_address = [
37
+ xml_data.xpath('RltdPties/Dbtr/PstlAdr'),
38
+ xml_data.xpath('RltdPties/Dbtr/Pty/PstlAdr'),
39
+ ].reject(&:empty?).first
40
+
41
+ return nil if postal_address == nil || postal_address.empty?
42
+
43
+ @address ||= SepaFileParser::PostalAddress.new(postal_address)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SepaFileParser
4
+ class Entry
5
+
6
+ attr_reader :xml_data
7
+
8
+ def initialize(xml_data)
9
+ @xml_data = xml_data
10
+ @amount = xml_data.xpath('Amt/text()').text
11
+ end
12
+
13
+ def amount
14
+ SepaFileParser::Misc.to_amount(@amount)
15
+ end
16
+
17
+ def amount_in_cents
18
+ SepaFileParser::Misc.to_amount_in_cents(@amount)
19
+ end
20
+
21
+ # @return [String]
22
+ def currency
23
+ @currency ||= xml_data.xpath('Amt/@Ccy').text
24
+ end
25
+
26
+ # @return [Boolean]
27
+ def debit
28
+ @debit ||= xml_data.xpath('CdtDbtInd/text()').text.upcase == 'DBIT'
29
+ end
30
+
31
+ # @return [Date]
32
+ def value_date
33
+ @value_date ||= ((date = xml_data.xpath('ValDt/Dt/text()').text).empty? ? nil : Date.parse(date))
34
+ end
35
+
36
+ # @return [Date]
37
+ def booking_date
38
+ @booking_date ||= ((date = xml_data.xpath('BookgDt/Dt/text()').text).empty? ? nil : Date.parse(date))
39
+ end
40
+
41
+ # @return [DateTime]
42
+ def value_datetime
43
+ @value_datetime ||= ((datetime = xml_data.xpath('ValDt/DtTm/text()').text).empty? ? nil : DateTime.parse(datetime))
44
+ end
45
+
46
+ # @return [DateTime]
47
+ def booking_datetime
48
+ @booking_datetime ||= ((datetime = xml_data.xpath('BookgDt/DtTm/text()').text).empty? ? nil : DateTime.parse(datetime))
49
+ end
50
+
51
+ # @return [String]
52
+ def bank_reference # May be missing
53
+ @bank_reference ||= xml_data.xpath('AcctSvcrRef/text()').text
54
+ end
55
+
56
+ # @return [Array<SepaFileParser::Transaction>]
57
+ def transactions
58
+ @transactions ||= parse_transactions
59
+ end
60
+
61
+ # @return [Boolean]
62
+ def credit?
63
+ !debit
64
+ end
65
+
66
+ # @return [Boolean]
67
+ def debit?
68
+ debit
69
+ end
70
+
71
+ # @return [Integer] either 1 or -1
72
+ def sign
73
+ credit? ? 1 : -1
74
+ end
75
+
76
+ # @return [Boolean]
77
+ def reversal?
78
+ @reversal ||= xml_data.xpath('RvslInd/text()').text.downcase == 'true'
79
+ end
80
+
81
+ # @return [Boolean]
82
+ def booked?
83
+ @booked ||= xml_data.xpath('Sts/text()').text.upcase == 'BOOK'
84
+ end
85
+
86
+ # @return [String]
87
+ def additional_information
88
+ @additional_information ||= xml_data.xpath('AddtlNtryInf/text()').text
89
+ end
90
+ alias_method :description, :additional_information
91
+
92
+ # @return [SepaFileParser::Charges]
93
+ def charges
94
+ @charges ||= SepaFileParser::Charges.new(xml_data.xpath('Chrgs'))
95
+ end
96
+ # @return [SepaFileParser::BatchDetail, nil]
97
+ def batch_detail
98
+ @batch_detail ||= xml_data.xpath('NtryDtls/Btch').empty? ? nil : SepaFileParser::BatchDetail.new(@xml_data.xpath('NtryDtls/Btch'))
99
+ end
100
+
101
+ private
102
+
103
+ def parse_transactions
104
+ transaction_details = xml_data.xpath('NtryDtls/TxDtls')
105
+
106
+ amt = nil
107
+ ccy = nil
108
+
109
+ if transaction_details.length == 1
110
+ amt = xml_data.xpath('Amt/text()').text
111
+ ccy = xml_data.xpath('Amt/@Ccy').text
112
+ end
113
+
114
+ xml_data.xpath('NtryDtls/TxDtls').map { |x| Transaction.new(x, debit?, amt, ccy) }
115
+ end
116
+ end
117
+ end