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 +4 -4
- data/.github/workflows/ci.yml +1 -1
- data/.github/workflows/release.yml +2 -2
- data/README.md +47 -13
- data/brot.gemspec +2 -2
- data/lib/brot/document.rb +1 -1
- data/lib/brot/pain_version.rb +7 -7
- data/lib/brot/serializer_base.rb +3 -3
- data/lib/brot/serializers/{pain00100303.rb → pain00100103.rb} +3 -3
- data/lib/brot/transfer.rb +2 -0
- data/lib/brot/utils.rb +7 -0
- data/lib/brot/version.rb +1 -1
- data/lib/brot.rb +1 -1
- data/test/currency_support_test.rb +102 -0
- data/test/document_test.rb +25 -7
- data/test/transfer_test.rb +33 -0
- data/xsd/pain.001.001.03.xsd +921 -0
- metadata +7 -5
- data/xsd/pain.001.003.03.xsd +0 -474
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 11ae2c0176f01320f17c5755596448f27c35b258c911ade2f6e4f4fba2096273
|
|
4
|
+
data.tar.gz: b985c0fdb460926d4c30c26c9c47f1815a061545ae1fac068f8d6584bd0c1fdd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ab2e95235f9d24a157076fee29ceb68499c845a43d4517c0f3a6ef2dbc0f233d7c7f4990ba443487c6d5b8e1c4a0cc00ad12415455d11c60fe6d197306ca0f53
|
|
7
|
+
data.tar.gz: 3664f7b067f59f1c62a7013e117b8521560e169cda40b3713eaba6178bd3e951dd178f3e6216621b38f4e984fab7ceddbc99b49a49f31b20fcff83cd98abe441
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -10,7 +10,7 @@ jobs:
|
|
|
10
10
|
runs-on: ubuntu-latest
|
|
11
11
|
|
|
12
12
|
steps:
|
|
13
|
-
- uses: actions/checkout@
|
|
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@
|
|
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
|
|
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.
|
|
8
|
+
- `pain.001.001.03`
|
|
9
9
|
- `pain.001.001.12`
|
|
10
10
|
|
|
11
|
-
Use `Brot::PainVersion::
|
|
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::
|
|
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::
|
|
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
|
|
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::
|
|
235
|
-
Supported values are `Brot::PainVersion::
|
|
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.
|
|
249
|
-
`urn:iso:std:iso:20022:tech:xsd:pain.001.
|
|
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::
|
|
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
|
|
14
|
-
The output targets pain.001.
|
|
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::
|
|
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)
|
data/lib/brot/pain_version.rb
CHANGED
|
@@ -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
|
-
|
|
28
|
-
identifier: 'pain.001.
|
|
29
|
-
namespace: 'urn:iso:std:iso:20022:tech:xsd:pain.001.
|
|
30
|
-
serializer_class: Serializers::
|
|
31
|
-
xsd_path: File.expand_path('../../xsd/pain.001.
|
|
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
|
-
|
|
36
|
+
PAIN_001_001_03
|
|
37
37
|
].freeze
|
|
38
38
|
|
|
39
39
|
class << self
|
|
40
40
|
# @return [PainVersion]
|
|
41
41
|
def default
|
|
42
|
-
|
|
42
|
+
PAIN_001_001_03
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
# @return [Array<PainVersion>]
|
data/lib/brot/serializer_base.rb
CHANGED
|
@@ -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
|
|
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,
|
|
124
|
+
def amount(xml, transfer)
|
|
125
125
|
xml.Amt do
|
|
126
|
-
xml.InstdAmt(Utils.format_amount(amount), Ccy:
|
|
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.
|
|
6
|
-
class
|
|
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.
|
|
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
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/
|
|
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
|
data/test/document_test.rb
CHANGED
|
@@ -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::
|
|
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
|
|
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::
|
|
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::
|
|
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.
|
|
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::
|
|
122
|
-
|
|
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
|