sepa_file_parser 0.1.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.
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