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
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+
5
+ module SepaFileParser
6
+ class GroupHeader
7
+
8
+ attr_reader :message_id,
9
+ :creation_date_time,
10
+ :additional_information,
11
+ :message_pagination,
12
+ :xml_data
13
+
14
+ def initialize(xml_data)
15
+ @xml_data = xml_data
16
+ @message_id = xml_data.xpath('MsgId/text()').text
17
+ @creation_date_time = Time.parse(xml_data.xpath('CreDtTm/text()').text)
18
+ @message_pagination = (x = xml_data.xpath('MsgPgntn')).empty? ? nil : MessagePagination.new(x)
19
+ @additional_information = xml_data.xpath('AddtlInf/text()').text
20
+ end
21
+ end
22
+
23
+ class MessagePagination
24
+
25
+ attr_reader :page_number,
26
+ :xml_data
27
+
28
+ def initialize(xml_data)
29
+ @xml_data = xml_data
30
+ @page_number = xml_data.xpath('PgNb/text()').text.to_i
31
+ @last_page_indicator = xml_data.xpath('LastPgInd/text()').text == 'true'
32
+ end
33
+
34
+ def last_page?
35
+ @last_page_indicator
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SepaFileParser
4
+ class PainEntry
5
+
6
+ attr_reader :xml_data
7
+
8
+ def initialize(xml_data)
9
+ @xml_data = xml_data
10
+
11
+ @amount = [
12
+ xml_data.xpath('Amt/InstdAmt').first,
13
+ xml_data.xpath('InstdAmt').first,
14
+ ].find { |a| a != nil }.text
15
+ end
16
+
17
+ def amount
18
+ SepaFileParser::Misc.to_amount(@amount)
19
+ end
20
+
21
+ def amount_in_cents
22
+ SepaFileParser::Misc.to_amount_in_cents(@amount)
23
+ end
24
+
25
+ # @return [String]
26
+ def currency
27
+ @currency ||= [
28
+ xml_data.xpath('Amt/InstdAmt/@Ccy').first,
29
+ xml_data.xpath('InstdAmt/@Ccy').first,
30
+ ].find { |a| a != nil }.text
31
+ end
32
+
33
+ def account
34
+ @account ||= SepaFileParser::Account.from_pain_data(
35
+ xml_data,
36
+ currency,
37
+ 'Cdtr',
38
+ )
39
+ end
40
+
41
+ def remittance_information
42
+ @remittance_information ||= xml_data.xpath('RmtInf/Ustrd').text
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SepaFileParser
4
+ class PostalAddress
5
+
6
+ attr_reader :xml_data
7
+
8
+ def initialize(xml_data)
9
+ @xml_data = xml_data
10
+ end
11
+
12
+ # @return [Array<String>]
13
+ def lines # May be empty
14
+ xml_data.xpath('AdrLine').map do |x|
15
+ x.xpath('text()').text
16
+ end
17
+ end
18
+
19
+ # @return [String]
20
+ def street_name # May be missing
21
+ xml_data.xpath('StrtNm/text()').text
22
+ end
23
+
24
+ # @return [String]
25
+ def building_number # May be missing
26
+ xml_data.xpath('BldgNb/text()').text
27
+ end
28
+
29
+ # @return [String]
30
+ def postal_code # May be missing
31
+ xml_data.xpath('PstCd/text()').text
32
+ end
33
+
34
+ # @return [String]
35
+ def town_name # May be missing
36
+ xml_data.xpath('TwnNm/text()').text
37
+ end
38
+
39
+ # @return [String]
40
+ def country # May be missing
41
+ xml_data.xpath('Ctry/text()').text
42
+ end
43
+
44
+ end
45
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SepaFileParser
4
+ class Record
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
+ def currency
22
+ @currency ||= xml_data.xpath('Amt/@Ccy').text
23
+ end
24
+
25
+ def type
26
+ @type ||= SepaFileParser::Type::Builder.build_type(xml_data.xpath('Tp'))
27
+ end
28
+
29
+ def charges_included?
30
+ @charges_included ||= xml_data.xpath('ChrgInclInd/text()').text.downcase == 'true'
31
+ end
32
+
33
+ def debit
34
+ @debit ||= xml_data.xpath('CdtDbtInd/text()').text.upcase == 'DBIT'
35
+ end
36
+
37
+ def credit?
38
+ !debit
39
+ end
40
+
41
+ def debit?
42
+ debit
43
+ end
44
+
45
+ def sign
46
+ credit? ? 1 : -1
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SepaFileParser
4
+ class Transaction
5
+
6
+ attr_reader :xml_data
7
+
8
+ def initialize(xml_data, debit, amount = nil, currency = nil)
9
+ @xml_data = xml_data
10
+ @debit = debit
11
+ @amount = parse_amount || amount
12
+ @currency = parse_currency || currency
13
+ end
14
+
15
+ def amount
16
+ SepaFileParser::Misc.to_amount(@amount)
17
+ end
18
+
19
+ def amount_in_cents
20
+ SepaFileParser::Misc.to_amount_in_cents(@amount)
21
+ end
22
+
23
+ def currency
24
+ @currency
25
+ end
26
+
27
+ def creditor
28
+ @creditor ||= SepaFileParser::Creditor.new(xml_data)
29
+ end
30
+
31
+ def debitor
32
+ @debitor ||= SepaFileParser::Debitor.new(xml_data)
33
+ end
34
+
35
+ def name
36
+ credit? ? debitor.name : creditor.name
37
+ end
38
+
39
+ def iban
40
+ credit? ? debitor.iban : creditor.iban
41
+ end
42
+
43
+ def bic
44
+ credit? ? debitor.bic : creditor.bic
45
+ end
46
+
47
+ def postal_address
48
+ credit? ? debitor.postal_address : creditor.postal_address
49
+ end
50
+
51
+ def credit?
52
+ !debit
53
+ end
54
+
55
+ def debit?
56
+ debit
57
+ end
58
+
59
+ def debit
60
+ @debit
61
+ end
62
+
63
+ def sign
64
+ credit? ? 1 : -1
65
+ end
66
+
67
+ def remittance_information
68
+ @remittance_information ||= begin
69
+ if (x = xml_data.xpath('RmtInf/Ustrd')).empty?
70
+ nil
71
+ else
72
+ x.collect(&:content).join(' ')
73
+ end
74
+ end
75
+ end
76
+
77
+ def swift_code
78
+ @swift_code ||= xml_data.xpath('BkTxCd/Prtry/Cd/text()').text.split('+')[0]
79
+ end
80
+
81
+ def reference
82
+ @reference ||= xml_data.xpath('Refs/InstrId/text()').text
83
+ end
84
+
85
+ def bank_reference # May be missing
86
+ @bank_reference ||= xml_data.xpath('Refs/AcctSvcrRef/text()').text
87
+ end
88
+
89
+ def end_to_end_reference # May be missing
90
+ @end_to_end_reference ||= xml_data.xpath('Refs/EndToEndId/text()').text
91
+ end
92
+
93
+ def mandate_reference # May be missing
94
+ @mandate_reference ||= xml_data.xpath('Refs/MndtId/text()').text
95
+ end
96
+
97
+ def creditor_reference # May be missing
98
+ @creditor_reference ||= xml_data.xpath('RmtInf/Strd/CdtrRefInf/Ref/text()').text
99
+ end
100
+
101
+ def transaction_id # May be missing
102
+ @transaction_id ||= xml_data.xpath('Refs/TxId/text()').text
103
+ end
104
+
105
+ def creditor_identifier # May be missing
106
+ @creditor_identifier ||= xml_data.xpath('RltdPties/Cdtr/Id/PrvtId/Othr/Id/text()').text
107
+ end
108
+
109
+ def payment_information # May be missing
110
+ @payment_information ||= xml_data.xpath('Refs/PmtInfId/text()').text
111
+ end
112
+
113
+ def additional_information # May be missing
114
+ @addition_information ||= xml_data.xpath('AddtlTxInf/text()').text
115
+ end
116
+
117
+ def reason_code # May be missing
118
+ @reason_code ||= xml_data.xpath('RtrInf/Rsn/Cd/text()').text
119
+ end
120
+
121
+ private
122
+
123
+ def parse_amount
124
+ if xml_data.xpath('Amt').any?
125
+ xml_data.xpath('Amt/text()').text
126
+ elsif xml_data.xpath('AmtDtls').any?
127
+ xml_data.xpath('AmtDtls//Amt/text()').first.text
128
+ end
129
+ end
130
+
131
+ def parse_currency
132
+ if xml_data.xpath('Amt').any?
133
+ xml_data.xpath('Amt/@Ccy').text
134
+ elsif xml_data.xpath('AmtDtls').any?
135
+ xml_data.xpath('AmtDtls//Amt/@Ccy').first.text
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SepaFileParser
4
+ module Type
5
+ class Builder
6
+ def self.build_type(xml_data)
7
+ if xml_data.xpath('Prtry').any?
8
+ Proprietary.new(
9
+ xml_data.xpath('Prtry/Id/text()').text,
10
+ xml_data.xpath('Prtry/Issr/text()').text
11
+ )
12
+ elsif xml_data.xpath('Cd').any?
13
+ Code.new(xml_data.xpath('Cd/text()').text)
14
+ else
15
+ nil
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SepaFileParser
4
+ module Type
5
+ class Code
6
+ attr_reader :code
7
+
8
+ def initialize(code)
9
+ @code = code
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SepaFileParser
4
+ module Type
5
+ class Proprietary
6
+ attr_reader :id, :issuer
7
+
8
+ def initialize(id, issuer)
9
+ @id, @issuer = id, issuer
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SepaFileParser
4
+ class Misc
5
+ class << self
6
+
7
+ # @param value [nil, String]
8
+ # @return [Integer, nil]
9
+ def to_amount_in_cents(value)
10
+ return nil if value == nil || value.strip == ''
11
+
12
+ # Using dollars and cents as representation for parts before and after the decimal separator
13
+ dollars, cents = value.split(/,|\./)
14
+ cents ||= '0'
15
+ format('%s%s', dollars, cents.ljust(2, '0')).to_i
16
+ end
17
+
18
+ # @param value [nil, String]
19
+ # @return [BigDecimal, nil]
20
+ def to_amount(value)
21
+ return nil if value == nil || value.strip == ''
22
+
23
+ BigDecimal(value.tr(',', '.'))
24
+ end
25
+
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SepaFileParser
4
+ module Pain001
5
+ class Base
6
+ attr_reader :group_header, :payment_informations, :xml_data
7
+
8
+ def initialize(xml_data)
9
+ @xml_data = xml_data
10
+ # CstmrCdtTrfInitn = Customer Credit Transaction Initiation
11
+ grphdr = xml_data.xpath('CstmrCdtTrfInitn/GrpHdr')
12
+ @group_header = SepaFileParser::GroupHeader.new(grphdr)
13
+ payment_informations = xml_data.xpath('CstmrCdtTrfInitn/PmtInf')
14
+ @payment_informations = payment_informations.map{ |x| PaymentInformation.new(x) }
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SepaFileParser
4
+ module Pain001
5
+ class PaymentInformation
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('PmtInfId/text()').text
15
+ end
16
+
17
+ def requested_execution_date
18
+ @requested_execution_date ||= Time.parse(xml_data.xpath('ReqdExctnDt/text()').text)
19
+ end
20
+
21
+ def account
22
+ @account ||= SepaFileParser::Account.from_pain_data(
23
+ xml_data,
24
+ entries.first.currency,
25
+ 'Dbtr',
26
+ )
27
+ end
28
+
29
+ def entries
30
+ @entries ||= xml_data.xpath('CdtTrfTxInf').map do |x|
31
+ SepaFileParser::PainEntry.new(x)
32
+ end
33
+ end
34
+
35
+ def source
36
+ xml_data.to_s
37
+ end
38
+
39
+ def self.parse(xml)
40
+ self.new(Nokogiri::XML(xml).xpath('PmtInf'))
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SepaFileParser
4
+ module Pain008
5
+ class Base
6
+ attr_reader :group_header, :payment_informations, :xml_data
7
+
8
+ def initialize(xml_data)
9
+ @xml_data = xml_data
10
+ # CstmrDrctDbtInitn = Customer Direct Debig Initiation
11
+ grphdr = xml_data.xpath('CstmrDrctDbtInitn/GrpHdr')
12
+ @group_header = SepaFileParser::GroupHeader.new(grphdr)
13
+ payment_informations = xml_data.xpath('CstmrDrctDbtInitn/PmtInf')
14
+ @payment_informations = payment_informations.map{ |x| PaymentInformation.new(x) }
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SepaFileParser
4
+ module Pain008
5
+ class PaymentInformation
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('PmtInfId/text()').text
15
+ end
16
+
17
+ def requested_collection_date
18
+ @requested_collection_date ||= Time.parse(xml_data.xpath('ReqdColltnDt/text()').text)
19
+ end
20
+
21
+ def account
22
+ @account ||= SepaFileParser::Account.from_pain_data(
23
+ xml_data,
24
+ entries.first.currency,
25
+ 'Cdtr',
26
+ )
27
+ end
28
+
29
+ def entries
30
+ @entries ||= xml_data.xpath('DrctDbtTxInf').map do |x|
31
+ SepaFileParser::PainEntry.new(x)
32
+ end
33
+ end
34
+
35
+ def source
36
+ xml_data.to_s
37
+ end
38
+
39
+ def self.parse(xml)
40
+ self.new(Nokogiri::XML(xml).xpath('PmtInf'))
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './xml'
4
+
5
+ # Add registrations
6
+ ## CAMT052
7
+ SepaFileParser::Xml.register('urn:iso:std:iso:20022:tech:xsd:camt.052.001.02', :camt052)
8
+
9
+ ## CAMT053
10
+ SepaFileParser::Xml.register('urn:iso:std:iso:20022:tech:xsd:camt.053.001.02', :camt053)
11
+ SepaFileParser::Xml.register('urn:iso:std:iso:20022:tech:xsd:camt.053.001.04', :camt053)
12
+ SepaFileParser::Xml.register('urn:iso:std:iso:20022:tech:xsd:camt.053.001.08', :camt053)
13
+ SepaFileParser::Xml.register('urn:iso:std:iso:20022:tech:xsd:camt.053.001.10', :camt053)
14
+
15
+ ## CAMT054
16
+ SepaFileParser::Xml.register('urn:iso:std:iso:20022:tech:xsd:camt.054.001.02', :camt054)
17
+ SepaFileParser::Xml.register('urn:iso:std:iso:20022:tech:xsd:camt.054.001.04', :camt054)
18
+
19
+ ## PAIN001
20
+ SepaFileParser::Xml.register('urn:iso:std:iso:20022:tech:xsd:pain.001.001.03', :pain001)
21
+
22
+ ## PAIN008
23
+ SepaFileParser::Xml.register('urn:iso:std:iso:20022:tech:xsd:pain.008.003.02', :pain008)
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SepaFileParser
4
+ class String
5
+ def self.parse(raw_camt)
6
+ doc = Nokogiri::XML raw_camt
7
+ SepaFileParser::Xml.parse(doc)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SepaFileParser
4
+ VERSION = '0.1.0'.freeze
5
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SepaFileParser
4
+ class Xml
5
+ PARSER_MAPPING = {
6
+ camt052: SepaFileParser::Camt052::Base,
7
+ camt053: SepaFileParser::Camt053::Base,
8
+ camt054: SepaFileParser::Camt054::Base,
9
+ pain001: SepaFileParser::Pain001::Base,
10
+ pain008: SepaFileParser::Pain008::Base,
11
+ }.freeze
12
+
13
+ SUPPORTED_PARSERS = PARSER_MAPPING.keys.freeze
14
+
15
+ @namespace_parsers = {}
16
+
17
+ def self.register(namespace, parser)
18
+ if !SUPPORTED_PARSERS.include?(parser)
19
+ raise SepaFileParser::Errors::UnsupportedParserClass, parser.to_s
20
+ end
21
+
22
+ if @namespace_parsers.key?(namespace) # Prevent overwriting existing registrations
23
+ raise SepaFileParser::Errors::NamespaceAlreadyRegistered, namespace
24
+ end
25
+
26
+ @namespace_parsers[namespace] = PARSER_MAPPING[parser]
27
+ end
28
+
29
+ def self.parse(doc)
30
+ if !doc.is_a?(Nokogiri::XML::Document)
31
+ raise SepaFileParser::Errors::NotXMLError, doc.class
32
+ end
33
+
34
+ namespace = doc.namespaces['xmlns']
35
+ doc.remove_namespaces!
36
+
37
+ parser_class = @namespace_parsers[namespace]
38
+ if parser_class == nil
39
+ raise SepaFileParser::Errors::UnsupportedNamespaceError, namespace
40
+ end
41
+
42
+ return parser_class.new(doc.xpath('Document'))
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'nokogiri'
4
+ require 'time'
5
+ require 'bigdecimal'
6
+
7
+ require_relative 'sepa_file_parser/misc'
8
+ require_relative 'sepa_file_parser/version'
9
+ require_relative 'sepa_file_parser/errors'
10
+ require_relative 'sepa_file_parser/general/account_balance'
11
+ require_relative 'sepa_file_parser/general/creditor'
12
+ require_relative 'sepa_file_parser/general/debitor'
13
+ require_relative 'sepa_file_parser/general/entry'
14
+ require_relative 'sepa_file_parser/general/record'
15
+ require_relative 'sepa_file_parser/general/charges'
16
+ require_relative 'sepa_file_parser/general/account'
17
+ require_relative 'sepa_file_parser/general/batch_detail'
18
+ require_relative 'sepa_file_parser/general/group_header'
19
+ require_relative 'sepa_file_parser/general/pain_entry'
20
+ require_relative 'sepa_file_parser/general/postal_address'
21
+ require_relative 'sepa_file_parser/general/transaction'
22
+ require_relative 'sepa_file_parser/general/type/builder'
23
+ require_relative 'sepa_file_parser/general/type/code'
24
+ require_relative 'sepa_file_parser/general/type/proprietary'
25
+ require_relative 'sepa_file_parser/camt052/report'
26
+ require_relative 'sepa_file_parser/camt052/base'
27
+ require_relative 'sepa_file_parser/camt053/statement'
28
+ require_relative 'sepa_file_parser/camt053/base'
29
+ require_relative 'sepa_file_parser/camt054/notification'
30
+ require_relative 'sepa_file_parser/camt054/base'
31
+ require_relative 'sepa_file_parser/pain001/payment_information'
32
+ require_relative 'sepa_file_parser/pain001/base'
33
+ require_relative 'sepa_file_parser/pain008/payment_information'
34
+ require_relative 'sepa_file_parser/pain008/base'
35
+ require_relative 'sepa_file_parser/file'
36
+ require_relative 'sepa_file_parser/string'
37
+ require_relative 'sepa_file_parser/register'
38
+ require_relative 'sepa_file_parser/xml'