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.
@@ -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