brot 0.1.1 → 0.3.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 +4 -4
- data/.github/workflows/ci.yml +1 -1
- data/.github/workflows/release.yml +2 -2
- data/.rubocop.yml +5 -0
- data/README.md +169 -269
- data/brot.gemspec +2 -2
- data/lib/brot/account.rb +22 -0
- data/lib/brot/document.rb +105 -0
- data/lib/brot/pain_version.rb +83 -0
- data/lib/brot/schema.rb +60 -0
- data/lib/brot/serializer_base.rb +169 -0
- data/lib/brot/serializers/pain00100103.rb +22 -0
- data/lib/brot/{pain00100112/serializer.rb → serializers/pain00100112.rb} +3 -6
- data/lib/brot/transfer.rb +45 -0
- data/lib/brot/utils.rb +91 -0
- data/lib/brot/version.rb +1 -1
- data/lib/brot.rb +10 -1
- data/test/currency_support_test.rb +102 -0
- data/test/document_test.rb +155 -0
- data/test/transfer_test.rb +33 -0
- data/xsd/pain.001.001.03.xsd +921 -0
- metadata +16 -12
- data/lib/brot/pain00100112/account.rb +0 -48
- data/lib/brot/pain00100112/document.rb +0 -162
- data/lib/brot/pain00100112/schema.rb +0 -58
- data/lib/brot/pain00100112/serializer_base.rb +0 -174
- data/lib/brot/pain00100112/transfer.rb +0 -96
- data/lib/brot/pain00100112/utils.rb +0 -92
- data/lib/brot/pain00100112.rb +0 -25
- data/test/pain00100112_document_test.rb +0 -87
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'test_helper'
|
|
4
|
+
|
|
5
|
+
class CurrencySupportTest < Minitest::Test
|
|
6
|
+
DOCUMENT_ATTRIBUTES = {
|
|
7
|
+
message_id: 'MSG-20260313-01',
|
|
8
|
+
payment_information_id: 'PMT-20260313-01',
|
|
9
|
+
initiating_party_name: 'Example Debtor GmbH',
|
|
10
|
+
debtor_name: 'Example Debtor GmbH',
|
|
11
|
+
debtor_iban: 'DE12500105170648489890',
|
|
12
|
+
debtor_bic: 'INGDDEFFXXX',
|
|
13
|
+
requested_execution_date: Date.new(2026, 3, 13),
|
|
14
|
+
created_at: Time.utc(2026, 3, 13, 12, 0, 0)
|
|
15
|
+
}.freeze
|
|
16
|
+
TRANSFER_ATTRIBUTES = {
|
|
17
|
+
amount: '1250.50',
|
|
18
|
+
creditor_name: 'Example Supplier GmbH',
|
|
19
|
+
creditor_iban: 'DE89370400440532013000',
|
|
20
|
+
creditor_bic: 'COBADEFFXXX',
|
|
21
|
+
end_to_end_id: 'INV-2026-0001',
|
|
22
|
+
remittance_information: 'Invoice 2026-0001',
|
|
23
|
+
instruction_id: 'INSTR-2026-0001'
|
|
24
|
+
}.freeze
|
|
25
|
+
TRANSFER_OVERRIDE_KEYS = %i[
|
|
26
|
+
amount
|
|
27
|
+
creditor_bic
|
|
28
|
+
creditor_iban
|
|
29
|
+
creditor_name
|
|
30
|
+
currency
|
|
31
|
+
end_to_end_id
|
|
32
|
+
instruction_id
|
|
33
|
+
purpose_code
|
|
34
|
+
remittance_information
|
|
35
|
+
].freeze
|
|
36
|
+
PAIN_001_001_12_NS = {
|
|
37
|
+
'ns' => 'urn:iso:std:iso:20022:tech:xsd:pain.001.001.12'
|
|
38
|
+
}.freeze
|
|
39
|
+
PAIN_001_001_03_NS = {
|
|
40
|
+
'ns' => 'urn:iso:std:iso:20022:tech:xsd:pain.001.001.03'
|
|
41
|
+
}.freeze
|
|
42
|
+
|
|
43
|
+
def test_serializes_transfer_currency_for_version_twelve
|
|
44
|
+
parsed = Nokogiri::XML(document(version: Brot::PainVersion::PAIN_001_001_12, currency: 'usd').to_xml)
|
|
45
|
+
currency = parsed.at_xpath('//ns:Amt/ns:InstdAmt', PAIN_001_001_12_NS).attribute('Ccy').value
|
|
46
|
+
|
|
47
|
+
assert_equal 'USD', currency
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def test_supports_mixed_currencies_for_version_twelve
|
|
51
|
+
document = mixed_currency_document_for_version_twelve
|
|
52
|
+
parsed = Nokogiri::XML(document.to_xml)
|
|
53
|
+
amounts = parsed.xpath('//ns:Amt/ns:InstdAmt', PAIN_001_001_12_NS)
|
|
54
|
+
currencies = amounts.map { it['Ccy'] }
|
|
55
|
+
|
|
56
|
+
assert_equal %w[EUR USD], currencies
|
|
57
|
+
assert_predicate document.validate, :valid?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def test_serializes_transfer_currency_for_version_three
|
|
61
|
+
parsed = Nokogiri::XML(document(version: Brot::PainVersion::PAIN_001_001_03, currency: 'usd').to_xml)
|
|
62
|
+
currency = parsed.at_xpath('//ns:Amt/ns:InstdAmt', PAIN_001_001_03_NS).attribute('Ccy').value
|
|
63
|
+
|
|
64
|
+
assert_equal 'USD', currency
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def document(**overrides)
|
|
70
|
+
attributes = DOCUMENT_ATTRIBUTES.merge(version: Brot::PainVersion::PAIN_001_001_03)
|
|
71
|
+
attributes.merge!(overrides)
|
|
72
|
+
attributes[:transfers] ||= [transfer(**transfer_overrides(overrides))]
|
|
73
|
+
Brot::Document.new(**attributes)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def transfer(**overrides) = Brot::Transfer.new(**TRANSFER_ATTRIBUTES, **overrides)
|
|
77
|
+
|
|
78
|
+
def transfer_overrides(overrides) = overrides.slice(*TRANSFER_OVERRIDE_KEYS)
|
|
79
|
+
|
|
80
|
+
def mixed_currency_document_for_version_twelve
|
|
81
|
+
document(
|
|
82
|
+
version: Brot::PainVersion::PAIN_001_001_12,
|
|
83
|
+
transfers: [
|
|
84
|
+
transfer(currency: 'EUR'),
|
|
85
|
+
usd_transfer
|
|
86
|
+
]
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def usd_transfer
|
|
91
|
+
transfer(
|
|
92
|
+
amount: '99.95',
|
|
93
|
+
currency: 'USD',
|
|
94
|
+
creditor_name: 'Example Vendor LLC',
|
|
95
|
+
creditor_iban: 'FR7630006000011234567890189',
|
|
96
|
+
creditor_bic: 'AGRIFRPP',
|
|
97
|
+
end_to_end_id: 'INV-2026-0002',
|
|
98
|
+
remittance_information: 'Invoice 2026-0002',
|
|
99
|
+
instruction_id: 'INSTR-2026-0002'
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'test_helper'
|
|
4
|
+
|
|
5
|
+
EXPECTED_DOCUMENT_NAMESPACES = {
|
|
6
|
+
Brot::PainVersion::PAIN_001_001_12 => 'urn:iso:std:iso:20022:tech:xsd:pain.001.001.12',
|
|
7
|
+
Brot::PainVersion::PAIN_001_001_03 => 'urn:iso:std:iso:20022:tech:xsd:pain.001.001.03'
|
|
8
|
+
}.freeze
|
|
9
|
+
DOCUMENT_ATTRIBUTES = {
|
|
10
|
+
message_id: 'MSG-20260313-01',
|
|
11
|
+
payment_information_id: 'PMT-20260313-01',
|
|
12
|
+
initiating_party_name: 'Example Debtor GmbH',
|
|
13
|
+
debtor_name: 'Example Debtor GmbH',
|
|
14
|
+
debtor_iban: 'DE12500105170648489890',
|
|
15
|
+
debtor_bic: 'INGDDEFFXXX',
|
|
16
|
+
requested_execution_date: Date.new(2026, 3, 13),
|
|
17
|
+
created_at: Time.utc(2026, 3, 13, 12, 0, 0)
|
|
18
|
+
}.freeze
|
|
19
|
+
TRANSFER_ATTRIBUTES = {
|
|
20
|
+
amount: '1250.50',
|
|
21
|
+
creditor_name: 'Example Supplier GmbH',
|
|
22
|
+
creditor_iban: 'DE89370400440532013000',
|
|
23
|
+
creditor_bic: 'COBADEFFXXX',
|
|
24
|
+
end_to_end_id: 'INV-2026-0001',
|
|
25
|
+
remittance_information: 'Invoice 2026-0001',
|
|
26
|
+
instruction_id: 'INSTR-2026-0001'
|
|
27
|
+
}.freeze
|
|
28
|
+
TRANSFER_OVERRIDE_KEYS = %i[
|
|
29
|
+
amount
|
|
30
|
+
creditor_bic
|
|
31
|
+
creditor_iban
|
|
32
|
+
creditor_name
|
|
33
|
+
currency
|
|
34
|
+
end_to_end_id
|
|
35
|
+
instruction_id
|
|
36
|
+
purpose_code
|
|
37
|
+
remittance_information
|
|
38
|
+
].freeze
|
|
39
|
+
|
|
40
|
+
class DocumentTest < Minitest::Test
|
|
41
|
+
def test_defaults_to_the_default_supported_version
|
|
42
|
+
assert_equal Brot::PainVersion::PAIN_001_001_03, document.version
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def test_accepts_a_pain_version_object
|
|
46
|
+
assert_equal Brot::PainVersion::PAIN_001_001_12,
|
|
47
|
+
document(version: Brot::PainVersion::PAIN_001_001_12).version
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def test_serializes_the_expected_namespace_for_each_version
|
|
51
|
+
EXPECTED_DOCUMENT_NAMESPACES.each do |version, namespace|
|
|
52
|
+
assert_equal namespace, parsed_document(version).root.namespace.href
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def test_serializes_group_header_values_for_each_version
|
|
57
|
+
assert_for_each_version('//ns:GrpHdr/ns:MsgId', 'MSG-20260313-01')
|
|
58
|
+
assert_for_each_version('//ns:GrpHdr/ns:NbOfTxs', '1')
|
|
59
|
+
assert_for_each_version('//ns:GrpHdr/ns:CtrlSum', '1250.50')
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def test_serializes_payment_metadata_for_each_version
|
|
63
|
+
assert_for_each_version('//ns:PmtTpInf/ns:SvcLvl/ns:Cd', 'SEPA')
|
|
64
|
+
assert_for_each_version('//ns:Amt/ns:InstdAmt', '1250.50')
|
|
65
|
+
|
|
66
|
+
EXPECTED_DOCUMENT_NAMESPACES.each_key do |version|
|
|
67
|
+
assert_equal 'EUR',
|
|
68
|
+
parsed_document(version).at_xpath('//ns:Amt/ns:InstdAmt', ns(version)).attribute('Ccy').value
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def test_serializes_remittance_information_for_each_version
|
|
73
|
+
assert_for_each_version('//ns:RmtInf/ns:Ustrd', 'Invoice 2026-0001')
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def test_serializes_version_specific_requested_execution_dates
|
|
77
|
+
assert_xpath(Brot::PainVersion::PAIN_001_001_12, '//ns:ReqdExctnDt/ns:Dt', '2026-03-13')
|
|
78
|
+
assert_xpath(Brot::PainVersion::PAIN_001_001_03, '//ns:ReqdExctnDt', '2026-03-13')
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def test_serializes_version_specific_bic_element_names
|
|
82
|
+
assert_xpath(Brot::PainVersion::PAIN_001_001_12, '//ns:DbtrAgt/ns:FinInstnId/ns:BICFI', 'INGDDEFFXXX')
|
|
83
|
+
assert_xpath(Brot::PainVersion::PAIN_001_001_03, '//ns:DbtrAgt/ns:FinInstnId/ns:BIC', 'INGDDEFFXXX')
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def test_serializes_notprovided_when_bic_is_missing_for_each_version
|
|
87
|
+
EXPECTED_DOCUMENT_NAMESPACES.each_key do |version|
|
|
88
|
+
parsed = Nokogiri::XML(document(version:, debtor_bic: nil, creditor_bic: nil).to_xml)
|
|
89
|
+
|
|
90
|
+
assert_equal 'NOTPROVIDED', parsed.at_xpath('//ns:DbtrAgt/ns:FinInstnId/ns:Othr/ns:Id', ns(version)).content
|
|
91
|
+
assert_equal 'NOTPROVIDED', parsed.at_xpath('//ns:CdtrAgt/ns:FinInstnId/ns:Othr/ns:Id', ns(version)).content
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def test_validates_generated_xml_against_the_bundled_xsd_for_each_version
|
|
96
|
+
EXPECTED_DOCUMENT_NAMESPACES.each_key do |version|
|
|
97
|
+
assert_path_exists Brot::Schema.bundled_xsd_path(version)
|
|
98
|
+
assert_predicate document(version:).validate, :valid?
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def test_schema_validate_infers_the_version_from_the_xml_namespace
|
|
103
|
+
assert_predicate Brot::Schema.validate(document.to_xml(pretty: false)), :valid?
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def test_validate_bang_returns_a_valid_result_for_documents_and_raw_xml
|
|
107
|
+
assert_predicate document.validate!, :valid?
|
|
108
|
+
assert_predicate Brot::Schema.validate!(document.to_xml(pretty: false)), :valid?
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def test_rejects_an_invalid_iban
|
|
112
|
+
error = assert_raises(Brot::ValidationError) { invalid_document }
|
|
113
|
+
|
|
114
|
+
assert_equal 'iban is invalid', error.message
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def test_rejects_an_unsupported_version
|
|
118
|
+
error = assert_raises(Brot::ValidationError) { document(version: :'pain.001.999.99') }
|
|
119
|
+
|
|
120
|
+
assert_equal 'version must be one of pain.001.001.12, pain.001.001.03', error.message
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def assert_for_each_version(xpath, expected)
|
|
126
|
+
EXPECTED_DOCUMENT_NAMESPACES.each_key { |version| assert_xpath(version, xpath, expected) }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def assert_xpath(version, xpath, expected)
|
|
130
|
+
assert_equal expected,
|
|
131
|
+
parsed_document(version).at_xpath(xpath, ns(version)).content
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def document(version: Brot::PainVersion::PAIN_001_001_03, **overrides)
|
|
135
|
+
transfer_overrides = transfer_attributes_from(overrides)
|
|
136
|
+
attributes = DOCUMENT_ATTRIBUTES.merge(transfers: [transfer(**transfer_overrides)], version:)
|
|
137
|
+
attributes[:debtor_bic] = overrides[:debtor_bic] if overrides.key?(:debtor_bic)
|
|
138
|
+
Brot::Document.new(**attributes)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def invalid_document = Brot::Document.new(**DOCUMENT_ATTRIBUTES, transfers: [transfer], debtor_iban: 'INVALID')
|
|
142
|
+
|
|
143
|
+
def transfer_attributes_from(overrides)
|
|
144
|
+
overrides.slice(*TRANSFER_OVERRIDE_KEYS)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def parsed_document(version)
|
|
148
|
+
@parsed_documents ||= {}
|
|
149
|
+
@parsed_documents[version] ||= Nokogiri::XML(document(version:).to_xml)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def transfer(**overrides) = Brot::Transfer.new(**TRANSFER_ATTRIBUTES, **overrides)
|
|
153
|
+
|
|
154
|
+
def ns(version) = { 'ns' => EXPECTED_DOCUMENT_NAMESPACES.fetch(version) }
|
|
155
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'test_helper'
|
|
4
|
+
|
|
5
|
+
class TransferTest < Minitest::Test
|
|
6
|
+
ATTRIBUTES = {
|
|
7
|
+
amount: '1250.50',
|
|
8
|
+
creditor_name: 'Example Supplier GmbH',
|
|
9
|
+
creditor_iban: 'DE89370400440532013000',
|
|
10
|
+
creditor_bic: 'COBADEFFXXX',
|
|
11
|
+
end_to_end_id: 'INV-2026-0001',
|
|
12
|
+
remittance_information: 'Invoice 2026-0001',
|
|
13
|
+
instruction_id: 'INSTR-2026-0001'
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
def test_defaults_currency_to_eur
|
|
17
|
+
assert_equal 'EUR', transfer.currency
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def test_normalizes_currency_to_uppercase
|
|
21
|
+
assert_equal 'USD', transfer(currency: 'usd').currency
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def test_rejects_an_invalid_currency
|
|
25
|
+
error = assert_raises(Brot::ValidationError) { transfer(currency: 'EURO') }
|
|
26
|
+
|
|
27
|
+
assert_equal 'currency is invalid', error.message
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def transfer(**overrides) = Brot::Transfer.new(**ATTRIBUTES, **overrides)
|
|
33
|
+
end
|