brot 0.2.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 554c5401cab39dd06ddee8d095edb4121621c9abc88cc57aacc2e53ef945a720
4
- data.tar.gz: e3f4dcc0317331c41f7f6f4228b4c26dc77f89b91826c80704e45fa56dfdc39a
3
+ metadata.gz: 11ae2c0176f01320f17c5755596448f27c35b258c911ade2f6e4f4fba2096273
4
+ data.tar.gz: b985c0fdb460926d4c30c26c9c47f1815a061545ae1fac068f8d6584bd0c1fdd
5
5
  SHA512:
6
- metadata.gz: c37b1944939b3808a7ff3fac28827b22f1228dbd5d3c10f3ce1728718a30d44374f6595cfe9900cda74f98f577db06871d30b988f27ffd4cb3d1a22e583fdff9
7
- data.tar.gz: 9ef486499ef15d58947fba7250905af25ebde97f45363d40d55b663d89beeaaa9898f5ae5bece7bc6d6bfb4db205c4722eb20820958d353caaa4c9b2fa83c263
6
+ metadata.gz: ab2e95235f9d24a157076fee29ceb68499c845a43d4517c0f3a6ef2dbc0f233d7c7f4990ba443487c6d5b8e1c4a0cc00ad12415455d11c60fe6d197306ca0f53
7
+ data.tar.gz: 3664f7b067f59f1c62a7013e117b8521560e169cda40b3713eaba6178bd3e951dd178f3e6216621b38f4e984fab7ceddbc99b49a49f31b20fcff83cd98abe441
@@ -11,7 +11,7 @@ jobs:
11
11
  runs-on: ubuntu-latest
12
12
 
13
13
  steps:
14
- - uses: actions/checkout@v4
14
+ - uses: actions/checkout@v6
15
15
 
16
16
  - uses: ruby/setup-ruby@v1
17
17
  with:
@@ -10,7 +10,7 @@ jobs:
10
10
  runs-on: ubuntu-latest
11
11
 
12
12
  steps:
13
- - uses: actions/checkout@v4
13
+ - uses: actions/checkout@v6
14
14
 
15
15
  - uses: ruby/setup-ruby@v1
16
16
  with:
@@ -33,7 +33,7 @@ jobs:
33
33
  contents: write
34
34
 
35
35
  steps:
36
- - uses: actions/checkout@v4
36
+ - uses: actions/checkout@v6
37
37
 
38
38
  - uses: ruby/setup-ruby@v1
39
39
  with:
data/README.md CHANGED
@@ -1,14 +1,14 @@
1
1
  # brot
2
2
 
3
- `brot` builds ISO 20022 SEPA credit transfer XML for DATEV-style uploads with
3
+ `brot` builds ISO 20022 credit transfer XML for DATEV-style uploads with
4
4
  `Nokogiri::XML::Builder`.
5
5
 
6
6
  The gem supports these bundled schema targets:
7
7
 
8
- - `pain.001.003.03`
8
+ - `pain.001.001.03`
9
9
  - `pain.001.001.12`
10
10
 
11
- Use `Brot::PainVersion::PAIN_001_003_03` or
11
+ Use `Brot::PainVersion::PAIN_001_001_03` or
12
12
  `Brot::PainVersion::PAIN_001_001_12` instead of raw symbols when selecting the
13
13
  output format.
14
14
 
@@ -16,6 +16,19 @@ The public API is intentionally narrow and DATEV-oriented. It models one
16
16
  debtor, one payment information block, one or more transfers, and unstructured
17
17
  remittance information.
18
18
 
19
+ ## DATEV Compatibility
20
+
21
+ Current DATEV compatibility and announced migration path are narrower than the
22
+ gem's bundled schema support:
23
+
24
+ - As of March 13, 2026, use `pain.001.001.03` for DATEV uploads.
25
+ - DATEV states that SEPA version 3.7 becomes mandatory in November 2026.
26
+ - Bundesbank maps SEPA version 3.6 to `pain.001.001.03` and SEPA version 3.7
27
+ to `pain.001.001.09`.
28
+ - `pain.001.001.09` is not implemented in this gem yet.
29
+ - `pain.001.001.12` remains available as a generic ISO 20022 output format,
30
+ but it should not be treated as a current DATEV upload target.
31
+
19
32
  ## Official References
20
33
 
21
34
  For authoritative field semantics and message context, use these primary
@@ -27,6 +40,11 @@ sources:
27
40
  - ISO 20022 message archive for older definitions, including
28
41
  `pain.001.001.03`:
29
42
  https://www.iso20022.org/catalogue-messages/iso-20022-messages-archive?search=pain
43
+ - DATEV note on SEPA version 3.7 becoming mandatory in November 2026:
44
+ https://www.datev.de/web/de/mydatev/datev-magazin/fuer-selbstbucher/neu-sepa-version-3-7/
45
+ - Deutsche Bundesbank format overview mapping SEPA versions to
46
+ `pain.001.001.03` and `pain.001.001.09`:
47
+ https://www.bundesbank.de/de/aufgaben/unbarer-zahlungsverkehr/serviceangebot/xml-formate-im-zv-613692
30
48
  - European Payments Council SCT rulebook and implementation-guideline entry
31
49
  point for SEPA-specific usage constraints and terminology:
32
50
  https://www.europeanpaymentscouncil.eu/what-we-do/epc-payment-schemes/sepa-credit-transfer/sepa-credit-transfer-rulebook-and
@@ -49,6 +67,7 @@ require 'date'
49
67
 
50
68
  transfer = Brot::Transfer.new(
51
69
  amount: '1250.50',
70
+ currency: 'EUR',
52
71
  creditor_name: 'Example Supplier GmbH',
53
72
  creditor_iban: 'DE89370400440532013000',
54
73
  creditor_bic: 'COBADEFFXXX',
@@ -66,7 +85,7 @@ document = Brot::Document.new(
66
85
  debtor_bic: 'INGDDEFFXXX',
67
86
  requested_execution_date: Date.new(2026, 3, 13),
68
87
  transfers: [transfer],
69
- version: Brot::PainVersion::PAIN_001_003_03
88
+ version: Brot::PainVersion::PAIN_001_001_03
70
89
  )
71
90
 
72
91
  xml = document.to_xml
@@ -76,9 +95,12 @@ raise result.errors.join("\n") unless result.valid?
76
95
  ```
77
96
 
78
97
  If you omit `version:`, the document defaults to
79
- `Brot::PainVersion::PAIN_001_003_03`.
98
+ `Brot::PainVersion::PAIN_001_001_03`.
80
99
  Call `document.validate!` if you prefer a raised `Brot::ValidationError`.
81
100
 
101
+ For current DATEV uploads, stick to
102
+ `Brot::PainVersion::PAIN_001_001_03`.
103
+
82
104
  ## Supported Subset
83
105
 
84
106
  Current assumptions baked into the serializer:
@@ -86,12 +108,19 @@ Current assumptions baked into the serializer:
86
108
  - Service level is always `SEPA`.
87
109
  - Payment method is always `TRF`.
88
110
  - Charge bearer is always `SLEV`.
89
- - Instructed amount is always emitted as `EUR`.
90
111
  - One debtor and one `PmtInf` block are emitted per document.
91
112
  - Remittance information is emitted as unstructured text only.
92
113
  - If a debtor BIC or creditor BIC is absent, the XML falls back to
93
114
  `NOTPROVIDED`.
94
115
 
116
+ Currency support:
117
+
118
+ - `Brot::Transfer` accepts `currency:` and defaults it to `EUR`.
119
+ - The bundled `pain.001.001.03` and `pain.001.001.12` schemas both emit the
120
+ supplied transfer currency and support mixed-currency documents.
121
+ - DATEV-specific acceptance is governed by DATEV's supported schema versions,
122
+ not by the gem alone.
123
+
95
124
  Supported input data:
96
125
 
97
126
  - Debtor and creditor names
@@ -100,6 +129,7 @@ Supported input data:
100
129
  - End-to-end IDs
101
130
  - Optional instruction IDs
102
131
  - Optional purpose codes
132
+ - Optional transfer currencies, defaulting to `EUR`
103
133
  - Unstructured remittance information
104
134
  - Validation against bundled XSDs for both supported versions
105
135
 
@@ -109,7 +139,7 @@ Not modeled:
109
139
  - Postal addresses and richer party identification
110
140
  - Ultimate debtor and ultimate creditor
111
141
  - Multiple `PmtInf` sections in one document
112
- - Non-SEPA service levels or non-EUR transfer setups
142
+ - Non-SEPA service levels
113
143
  - Broader ISO 20022 branches outside the current DATEV-oriented subset
114
144
 
115
145
  ## Public API
@@ -160,6 +190,7 @@ Example:
160
190
  ```ruby
161
191
  transfer = Brot::Transfer.new(
162
192
  amount: '1250.50',
193
+ currency: 'USD',
163
194
  creditor_name: 'Example Supplier GmbH',
164
195
  creditor_iban: 'DE89370400440532013000',
165
196
  creditor_bic: 'COBADEFFXXX',
@@ -180,6 +211,8 @@ Initialization attributes:
180
211
  Required. Validated as an IBAN.
181
212
  - `creditor_bic`
182
213
  Optional. Validated as a BIC if supplied.
214
+ - `currency`
215
+ Optional. Three-letter ISO-style currency code. Defaults to `EUR`.
183
216
  - `end_to_end_id`
184
217
  Required. Maximum 35 characters.
185
218
  - `remittance_information`
@@ -193,7 +226,7 @@ Initialization attributes:
193
226
 
194
227
  Use `Brot::Document` for the full payment file.
195
228
 
196
- Example using `.12` output:
229
+ Example using generic `.12` output:
197
230
 
198
231
  ```ruby
199
232
  document = Brot::Document.new(
@@ -231,8 +264,8 @@ Initialization attributes:
231
264
  - `created_at`
232
265
  Optional. Defaults to current UTC time.
233
266
  - `version`
234
- Optional. Defaults to `Brot::PainVersion::PAIN_001_003_03`.
235
- Supported values are `Brot::PainVersion::PAIN_001_003_03` and
267
+ Optional. Defaults to `Brot::PainVersion::PAIN_001_001_03`.
268
+ Supported values are `Brot::PainVersion::PAIN_001_001_03` and
236
269
  `Brot::PainVersion::PAIN_001_001_12`.
237
270
 
238
271
  Useful methods:
@@ -245,12 +278,13 @@ Useful methods:
245
278
 
246
279
  Version-specific output differences:
247
280
 
248
- - `pain.001.003.03` uses namespace
249
- `urn:iso:std:iso:20022:tech:xsd:pain.001.003.03`
281
+ - `pain.001.001.03` uses namespace
282
+ `urn:iso:std:iso:20022:tech:xsd:pain.001.001.03`
250
283
  - `pain.001.001.12` uses namespace
251
284
  `urn:iso:std:iso:20022:tech:xsd:pain.001.001.12`
252
285
  - `.03` writes `BIC` and plain `ReqdExctnDt`
253
286
  - `.12` writes `BICFI` and `ReqdExctnDt/Dt`
287
+ - For DATEV compatibility today, prefer `.03`
254
288
 
255
289
  ### `Brot::Schema`
256
290
 
@@ -286,5 +320,5 @@ from the XML document namespace.
286
320
  `Brot::PainVersion` is the public value object for supported output formats.
287
321
  Prefer these constants when constructing documents or looking up bundled XSDs:
288
322
 
289
- - `Brot::PainVersion::PAIN_001_003_03`
323
+ - `Brot::PainVersion::PAIN_001_001_03`
290
324
  - `Brot::PainVersion::PAIN_001_001_12`
data/brot.gemspec CHANGED
@@ -10,8 +10,8 @@ Gem::Specification.new do |spec|
10
10
 
11
11
  spec.summary = 'Build ISO 20022 pain.001 payment initiation XML for DATEV uploads.'
12
12
  spec.description = <<~TEXT
13
- brot builds SEPA credit transfer XML with Nokogiri::XML::Builder.
14
- The output targets pain.001.003.03 or pain.001.001.12 for DATEV-oriented uploads.
13
+ brot builds ISO 20022 credit transfer XML with Nokogiri::XML::Builder.
14
+ The output targets pain.001.001.03 or pain.001.001.12 for DATEV-oriented uploads.
15
15
  TEXT
16
16
  spec.homepage = 'https://github.com/garriguv/brot'
17
17
  spec.license = 'MIT'
data/lib/brot/document.rb CHANGED
@@ -16,7 +16,7 @@ module Brot
16
16
 
17
17
  # @param attributes [Hash]
18
18
  # @option attributes [PainVersion, Symbol, String] :version
19
- # Defaults to {PainVersion::PAIN_001_003_03}.
19
+ # Defaults to {PainVersion::PAIN_001_001_03}.
20
20
  def initialize(**attributes)
21
21
  @version = PainVersion.fetch(attributes.fetch(:version, PainVersion.default))
22
22
  assign_identifiers(attributes)
@@ -24,22 +24,22 @@ module Brot
24
24
  xsd_path: File.expand_path('../../xsd/pain.001.001.12.xsd', __dir__)
25
25
  )
26
26
 
27
- PAIN_001_003_03 = new(
28
- identifier: 'pain.001.003.03',
29
- namespace: 'urn:iso:std:iso:20022:tech:xsd:pain.001.003.03',
30
- serializer_class: Serializers::Pain00100303,
31
- xsd_path: File.expand_path('../../xsd/pain.001.003.03.xsd', __dir__)
27
+ PAIN_001_001_03 = new(
28
+ identifier: 'pain.001.001.03',
29
+ namespace: 'urn:iso:std:iso:20022:tech:xsd:pain.001.001.03',
30
+ serializer_class: Serializers::Pain00100103,
31
+ xsd_path: File.expand_path('../../xsd/pain.001.001.03.xsd', __dir__)
32
32
  )
33
33
 
34
34
  ALL = [
35
35
  PAIN_001_001_12,
36
- PAIN_001_003_03
36
+ PAIN_001_001_03
37
37
  ].freeze
38
38
 
39
39
  class << self
40
40
  # @return [PainVersion]
41
41
  def default
42
- PAIN_001_003_03
42
+ PAIN_001_001_03
43
43
  end
44
44
 
45
45
  # @return [Array<PainVersion>]
@@ -76,7 +76,7 @@ module Brot
76
76
  def build_credit_transfer(xml, transfer)
77
77
  xml.CdtTrfTxInf do
78
78
  payment_identification(xml, transfer)
79
- amount(xml, transfer.amount)
79
+ amount(xml, transfer)
80
80
  agent(xml, :CdtrAgt, transfer.creditor_account)
81
81
  creditor(xml, transfer)
82
82
  purpose(xml, transfer.purpose_code)
@@ -121,9 +121,9 @@ module Brot
121
121
  end
122
122
  end
123
123
 
124
- def amount(xml, amount)
124
+ def amount(xml, transfer)
125
125
  xml.Amt do
126
- xml.InstdAmt(Utils.format_amount(amount), Ccy: 'EUR')
126
+ xml.InstdAmt(Utils.format_amount(transfer.amount), Ccy: transfer.currency)
127
127
  end
128
128
  end
129
129
 
@@ -2,8 +2,8 @@
2
2
 
3
3
  module Brot
4
4
  module Serializers
5
- # Concrete serializer for pain.001.003.03 output.
6
- class Pain00100303 < SerializerBase
5
+ # Concrete serializer for pain.001.001.03 output.
6
+ class Pain00100103 < SerializerBase
7
7
  private
8
8
 
9
9
  def bic_element_name
@@ -11,7 +11,7 @@ module Brot
11
11
  end
12
12
 
13
13
  def namespace
14
- 'urn:iso:std:iso:20022:tech:xsd:pain.001.003.03'
14
+ 'urn:iso:std:iso:20022:tech:xsd:pain.001.001.03'
15
15
  end
16
16
 
17
17
  def requested_execution_date(xml)
data/lib/brot/transfer.rb CHANGED
@@ -6,6 +6,7 @@ module Brot
6
6
  attr_reader :amount,
7
7
  :creditor_account,
8
8
  :creditor_name,
9
+ :currency,
9
10
  :end_to_end_id,
10
11
  :instruction_id,
11
12
  :purpose_code,
@@ -21,6 +22,7 @@ module Brot
21
22
 
22
23
  def assign_creditor_details(attributes)
23
24
  @amount = Utils.normalize_amount!(attributes.fetch(:amount))
25
+ @currency = Utils.normalize_currency!(attributes.fetch(:currency, 'EUR'))
24
26
  @creditor_name = Utils.normalize_text!('creditor_name', attributes.fetch(:creditor_name), max: 70)
25
27
  @creditor_account = Account.new(iban: attributes.fetch(:creditor_iban), bic: attributes[:creditor_bic])
26
28
  end
data/lib/brot/utils.rb CHANGED
@@ -37,6 +37,13 @@ module Brot
37
37
  raise ValidationError, 'amount is invalid'
38
38
  end
39
39
 
40
+ def normalize_currency!(currency)
41
+ value = String(currency).upcase.strip
42
+ raise ValidationError, 'currency is invalid' unless /\A[A-Z]{3}\z/.match?(value)
43
+
44
+ value
45
+ end
46
+
40
47
  def normalize_date!(date)
41
48
  return date if date.is_a?(Date)
42
49
  return Date.iso8601(date) if date.is_a?(String)
data/lib/brot/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Brot
4
- VERSION = '0.2.0'
4
+ VERSION = '0.3.0'
5
5
  end
data/lib/brot.rb CHANGED
@@ -16,7 +16,7 @@ require_relative 'brot/account'
16
16
  require_relative 'brot/transfer'
17
17
  require_relative 'brot/serializer_base'
18
18
  require_relative 'brot/serializers/pain00100112'
19
- require_relative 'brot/serializers/pain00100303'
19
+ require_relative 'brot/serializers/pain00100103'
20
20
  require_relative 'brot/pain_version'
21
21
  require_relative 'brot/schema'
22
22
  require_relative 'brot/document'
@@ -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
@@ -4,7 +4,7 @@ require_relative 'test_helper'
4
4
 
5
5
  EXPECTED_DOCUMENT_NAMESPACES = {
6
6
  Brot::PainVersion::PAIN_001_001_12 => 'urn:iso:std:iso:20022:tech:xsd:pain.001.001.12',
7
- Brot::PainVersion::PAIN_001_003_03 => 'urn:iso:std:iso:20022:tech:xsd:pain.001.003.03'
7
+ Brot::PainVersion::PAIN_001_001_03 => 'urn:iso:std:iso:20022:tech:xsd:pain.001.001.03'
8
8
  }.freeze
9
9
  DOCUMENT_ATTRIBUTES = {
10
10
  message_id: 'MSG-20260313-01',
@@ -25,9 +25,22 @@ TRANSFER_ATTRIBUTES = {
25
25
  remittance_information: 'Invoice 2026-0001',
26
26
  instruction_id: 'INSTR-2026-0001'
27
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
28
39
 
29
40
  class DocumentTest < Minitest::Test
30
- def test_defaults_to_the_latest_supported_version = assert_equal(Brot::PainVersion::PAIN_001_003_03, document.version)
41
+ def test_defaults_to_the_default_supported_version
42
+ assert_equal Brot::PainVersion::PAIN_001_001_03, document.version
43
+ end
31
44
 
32
45
  def test_accepts_a_pain_version_object
33
46
  assert_equal Brot::PainVersion::PAIN_001_001_12,
@@ -62,12 +75,12 @@ class DocumentTest < Minitest::Test
62
75
 
63
76
  def test_serializes_version_specific_requested_execution_dates
64
77
  assert_xpath(Brot::PainVersion::PAIN_001_001_12, '//ns:ReqdExctnDt/ns:Dt', '2026-03-13')
65
- assert_xpath(Brot::PainVersion::PAIN_001_003_03, '//ns:ReqdExctnDt', '2026-03-13')
78
+ assert_xpath(Brot::PainVersion::PAIN_001_001_03, '//ns:ReqdExctnDt', '2026-03-13')
66
79
  end
67
80
 
68
81
  def test_serializes_version_specific_bic_element_names
69
82
  assert_xpath(Brot::PainVersion::PAIN_001_001_12, '//ns:DbtrAgt/ns:FinInstnId/ns:BICFI', 'INGDDEFFXXX')
70
- assert_xpath(Brot::PainVersion::PAIN_001_003_03, '//ns:DbtrAgt/ns:FinInstnId/ns:BIC', 'INGDDEFFXXX')
83
+ assert_xpath(Brot::PainVersion::PAIN_001_001_03, '//ns:DbtrAgt/ns:FinInstnId/ns:BIC', 'INGDDEFFXXX')
71
84
  end
72
85
 
73
86
  def test_serializes_notprovided_when_bic_is_missing_for_each_version
@@ -104,7 +117,7 @@ class DocumentTest < Minitest::Test
104
117
  def test_rejects_an_unsupported_version
105
118
  error = assert_raises(Brot::ValidationError) { document(version: :'pain.001.999.99') }
106
119
 
107
- assert_equal 'version must be one of pain.001.001.12, pain.001.003.03', error.message
120
+ assert_equal 'version must be one of pain.001.001.12, pain.001.001.03', error.message
108
121
  end
109
122
 
110
123
  private
@@ -118,14 +131,19 @@ class DocumentTest < Minitest::Test
118
131
  parsed_document(version).at_xpath(xpath, ns(version)).content
119
132
  end
120
133
 
121
- def document(version: Brot::PainVersion::PAIN_001_003_03, **overrides)
122
- attributes = DOCUMENT_ATTRIBUTES.merge(transfers: [transfer(**overrides.slice(:creditor_bic))], version:)
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:)
123
137
  attributes[:debtor_bic] = overrides[:debtor_bic] if overrides.key?(:debtor_bic)
124
138
  Brot::Document.new(**attributes)
125
139
  end
126
140
 
127
141
  def invalid_document = Brot::Document.new(**DOCUMENT_ATTRIBUTES, transfers: [transfer], debtor_iban: 'INVALID')
128
142
 
143
+ def transfer_attributes_from(overrides)
144
+ overrides.slice(*TRANSFER_OVERRIDE_KEYS)
145
+ end
146
+
129
147
  def parsed_document(version)
130
148
  @parsed_documents ||= {}
131
149
  @parsed_documents[version] ||= Nokogiri::XML(document(version:).to_xml)
@@ -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