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.
- checksums.yaml +7 -0
- data/lib/sepa_file_parser/camt052/base.rb +18 -0
- data/lib/sepa_file_parser/camt052/report.rb +73 -0
- data/lib/sepa_file_parser/camt053/base.rb +18 -0
- data/lib/sepa_file_parser/camt053/statement.rb +75 -0
- data/lib/sepa_file_parser/camt054/base.rb +18 -0
- data/lib/sepa_file_parser/camt054/notification.rb +54 -0
- data/lib/sepa_file_parser/errors.rb +12 -0
- data/lib/sepa_file_parser/file.rb +10 -0
- data/lib/sepa_file_parser/general/account.rb +66 -0
- data/lib/sepa_file_parser/general/account_balance.rb +61 -0
- data/lib/sepa_file_parser/general/batch_detail.rb +24 -0
- data/lib/sepa_file_parser/general/charges.rb +25 -0
- data/lib/sepa_file_parser/general/creditor.rb +46 -0
- data/lib/sepa_file_parser/general/debitor.rb +46 -0
- data/lib/sepa_file_parser/general/entry.rb +117 -0
- data/lib/sepa_file_parser/general/group_header.rb +38 -0
- data/lib/sepa_file_parser/general/pain_entry.rb +46 -0
- data/lib/sepa_file_parser/general/postal_address.rb +45 -0
- data/lib/sepa_file_parser/general/record.rb +49 -0
- data/lib/sepa_file_parser/general/transaction.rb +139 -0
- data/lib/sepa_file_parser/general/type/builder.rb +20 -0
- data/lib/sepa_file_parser/general/type/code.rb +13 -0
- data/lib/sepa_file_parser/general/type/proprietary.rb +13 -0
- data/lib/sepa_file_parser/misc.rb +28 -0
- data/lib/sepa_file_parser/pain001/base.rb +18 -0
- data/lib/sepa_file_parser/pain001/payment_information.rb +44 -0
- data/lib/sepa_file_parser/pain008/base.rb +18 -0
- data/lib/sepa_file_parser/pain008/payment_information.rb +44 -0
- data/lib/sepa_file_parser/register.rb +23 -0
- data/lib/sepa_file_parser/string.rb +10 -0
- data/lib/sepa_file_parser/version.rb +5 -0
- data/lib/sepa_file_parser/xml.rb +45 -0
- data/lib/sepa_file_parser.rb +38 -0
- 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,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
|