brot 0.1.1
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 +7 -0
- data/.github/workflows/ci.yml +25 -0
- data/.github/workflows/release.yml +43 -0
- data/.rubocop.yml +9 -0
- data/Gemfile +13 -0
- data/LICENSE.txt +22 -0
- data/README.md +424 -0
- data/Rakefile +10 -0
- data/brot.gemspec +44 -0
- data/lib/brot/pain00100112/account.rb +48 -0
- data/lib/brot/pain00100112/document.rb +162 -0
- data/lib/brot/pain00100112/schema.rb +58 -0
- data/lib/brot/pain00100112/serializer.rb +25 -0
- data/lib/brot/pain00100112/serializer_base.rb +174 -0
- data/lib/brot/pain00100112/transfer.rb +96 -0
- data/lib/brot/pain00100112/utils.rb +92 -0
- data/lib/brot/pain00100112.rb +25 -0
- data/lib/brot/version.rb +5 -0
- data/lib/brot.rb +13 -0
- data/test/brot_test.rb +9 -0
- data/test/pain00100112_document_test.rb +87 -0
- data/test/test_helper.rb +5 -0
- data/xsd/pain.001.001.12.xsd +1208 -0
- metadata +82 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: bf6f5e4155bf59cb2cc724e9ea723bb066d5c95e745e687b66e36ad4d058d81f
|
|
4
|
+
data.tar.gz: 931cfa4d40375498939bdec52b97b9f63987e16e9efa825aec351987f7b97d6e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 46155574730e67a42bba7c1a815f1be3dcec94c2fda775a66b646b6237b6f51ee83f1d15907a4d4ed503ef9d0195316a42891d64acaa7326b83b1cca542651e5
|
|
7
|
+
data.tar.gz: 9e5c22fdb7710f461e28639032e8534c8aafed98c77943e8ba4c27460cda071db18a0bdb10b0f1f11036cd44e117d1acea521762fac85e3c7971a06fd78beaf7
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
pull_request:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- uses: ruby/setup-ruby@v1
|
|
17
|
+
with:
|
|
18
|
+
ruby-version: "4.0"
|
|
19
|
+
bundler-cache: true
|
|
20
|
+
|
|
21
|
+
- name: Run tests
|
|
22
|
+
run: bundle exec rake test
|
|
23
|
+
|
|
24
|
+
- name: Run RuboCop
|
|
25
|
+
run: bundle exec rubocop
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
|
|
15
|
+
- uses: ruby/setup-ruby@v1
|
|
16
|
+
with:
|
|
17
|
+
ruby-version: '4.0'
|
|
18
|
+
bundler-cache: true
|
|
19
|
+
|
|
20
|
+
- name: Run tests
|
|
21
|
+
run: bundle exec rake test
|
|
22
|
+
|
|
23
|
+
- name: Run RuboCop
|
|
24
|
+
run: bundle exec rubocop
|
|
25
|
+
|
|
26
|
+
release:
|
|
27
|
+
name: Publish to RubyGems
|
|
28
|
+
needs: [test]
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
environment: release
|
|
31
|
+
permissions:
|
|
32
|
+
id-token: write
|
|
33
|
+
contents: write
|
|
34
|
+
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/checkout@v4
|
|
37
|
+
|
|
38
|
+
- uses: ruby/setup-ruby@v1
|
|
39
|
+
with:
|
|
40
|
+
ruby-version: '4.0'
|
|
41
|
+
bundler-cache: true
|
|
42
|
+
|
|
43
|
+
- uses: rubygems/release-gem@v1
|
data/.rubocop.yml
ADDED
data/Gemfile
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
source 'https://rubygems.org'
|
|
4
|
+
|
|
5
|
+
gemspec
|
|
6
|
+
|
|
7
|
+
group :development do
|
|
8
|
+
gem 'minitest', '~> 5.25'
|
|
9
|
+
gem 'rake', '~> 13.3'
|
|
10
|
+
gem 'rubocop', '~> 1.76'
|
|
11
|
+
gem 'rubocop-minitest', '~> 0.38'
|
|
12
|
+
gem 'rubocop-rake', '~> 0.7'
|
|
13
|
+
end
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vincent Garrigues
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
data/README.md
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
# brot
|
|
2
|
+
|
|
3
|
+
`brot` builds ISO 20022 SEPA credit transfer XML for DATEV-style uploads with
|
|
4
|
+
`Nokogiri::XML::Builder`.
|
|
5
|
+
|
|
6
|
+
The gem emits `pain.001.001.12` and is designed to validate directly against
|
|
7
|
+
the provided `pain.001.001.12.xsd`.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem 'brot'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## What The Gem Does
|
|
16
|
+
|
|
17
|
+
`brot` is intentionally focused on one narrow job:
|
|
18
|
+
|
|
19
|
+
- Generate `pain.001.001.12` customer credit transfer files.
|
|
20
|
+
- Build XML with `Nokogiri::XML::Builder`.
|
|
21
|
+
- Validate generated XML against the bundled `pain.001.001.12.xsd`.
|
|
22
|
+
- Stay close to the DATEV upload use case instead of exposing the full ISO
|
|
23
|
+
20022 surface area.
|
|
24
|
+
|
|
25
|
+
Current assumptions baked into the serializer:
|
|
26
|
+
|
|
27
|
+
- Service level is always `SEPA`.
|
|
28
|
+
- Payment method is always `TRF`.
|
|
29
|
+
- Charge bearer is always `SLEV`.
|
|
30
|
+
- Instructed amount is always emitted as `EUR`.
|
|
31
|
+
- A debtor BIC or creditor BIC is optional. If absent, the XML uses the
|
|
32
|
+
fallback financial institution identifier `NOTPROVIDED`.
|
|
33
|
+
|
|
34
|
+
## Scope And Gaps
|
|
35
|
+
|
|
36
|
+
`brot` is intentionally a DATEV-oriented subset of `pain.001.001.12`, not a
|
|
37
|
+
full abstraction over every branch allowed by the ISO 20022 schema.
|
|
38
|
+
|
|
39
|
+
What is currently supported:
|
|
40
|
+
|
|
41
|
+
- SEPA credit transfers
|
|
42
|
+
- One debtor per document
|
|
43
|
+
- One payment information block (`PmtInf`) per document
|
|
44
|
+
- One or more credit transfer entries (`CdtTrfTxInf`)
|
|
45
|
+
- Debtor and creditor names
|
|
46
|
+
- Debtor and creditor IBANs
|
|
47
|
+
- Optional debtor and creditor BICs
|
|
48
|
+
- End-to-end IDs
|
|
49
|
+
- Optional instruction IDs
|
|
50
|
+
- Optional purpose codes
|
|
51
|
+
- Unstructured remittance information (`RmtInf/Ustrd`)
|
|
52
|
+
- Validation against the bundled `pain.001.001.12.xsd`
|
|
53
|
+
|
|
54
|
+
What is not modeled yet, even though the schema allows it:
|
|
55
|
+
|
|
56
|
+
- Structured remittance information
|
|
57
|
+
- Postal addresses and richer party identification data
|
|
58
|
+
- Ultimate debtor and ultimate creditor
|
|
59
|
+
- Multiple `PmtInf` sections in one document
|
|
60
|
+
- Non-SEPA service levels or non-EUR transfer setups
|
|
61
|
+
- Local instrument and category purpose variants beyond the current subset
|
|
62
|
+
- Intermediary agents and extra account layers
|
|
63
|
+
- Regulatory reporting, tax, cheque, and supplementary data sections
|
|
64
|
+
|
|
65
|
+
The practical meaning is:
|
|
66
|
+
|
|
67
|
+
- The gem aims to produce a clean DATEV-style payment initiation file for the
|
|
68
|
+
supported SEPA use case.
|
|
69
|
+
- The gem does not yet try to represent the entire `pain.001.001.12` standard
|
|
70
|
+
as a generic Ruby object model.
|
|
71
|
+
|
|
72
|
+
## Quick Start
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
require 'brot'
|
|
76
|
+
require 'date'
|
|
77
|
+
|
|
78
|
+
transfer = Brot::Pain00100112::Transfer.new(
|
|
79
|
+
amount: '1250.50',
|
|
80
|
+
creditor_name: 'Example Supplier GmbH',
|
|
81
|
+
creditor_iban: 'DE89370400440532013000',
|
|
82
|
+
creditor_bic: 'COBADEFFXXX',
|
|
83
|
+
end_to_end_id: 'INV-2026-0001',
|
|
84
|
+
remittance_information: 'Invoice 2026-0001',
|
|
85
|
+
instruction_id: 'PAY-0001'
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
document = Brot::Pain00100112::Document.new(
|
|
89
|
+
message_id: 'MSG-20260313-01',
|
|
90
|
+
payment_information_id: 'PMT-20260313-01',
|
|
91
|
+
initiating_party_name: 'Example Debtor GmbH',
|
|
92
|
+
debtor_name: 'Example Debtor GmbH',
|
|
93
|
+
debtor_iban: 'DE12500105170648489890',
|
|
94
|
+
debtor_bic: 'INGDDEFFXXX',
|
|
95
|
+
requested_execution_date: Date.new(2026, 3, 13),
|
|
96
|
+
transfers: [transfer],
|
|
97
|
+
batch_booking: true
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
xml = document.to_xml
|
|
101
|
+
result = document.validate
|
|
102
|
+
|
|
103
|
+
raise result.errors.join("\n") unless result.valid?
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Public API
|
|
107
|
+
|
|
108
|
+
The main public classes are:
|
|
109
|
+
|
|
110
|
+
- `Brot::Pain00100112::Account`
|
|
111
|
+
- `Brot::Pain00100112::Transfer`
|
|
112
|
+
- `Brot::Pain00100112::Document`
|
|
113
|
+
- `Brot::Pain00100112::Schema`
|
|
114
|
+
- `Brot::Pain00100112::Schema::Result`
|
|
115
|
+
|
|
116
|
+
### `Brot::Pain00100112::Account`
|
|
117
|
+
|
|
118
|
+
Use `Account` when you want a standalone object representing debtor or creditor
|
|
119
|
+
banking details. You do not need to instantiate it manually for normal usage,
|
|
120
|
+
because `Document` and `Transfer` build accounts from `*_iban` and `*_bic`
|
|
121
|
+
attributes internally.
|
|
122
|
+
|
|
123
|
+
Example:
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
account = Brot::Pain00100112::Account.new(
|
|
127
|
+
iban: 'DE89370400440532013000',
|
|
128
|
+
bic: 'COBADEFFXXX'
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
account.iban
|
|
132
|
+
# => "DE89370400440532013000"
|
|
133
|
+
|
|
134
|
+
account.bic
|
|
135
|
+
# => "COBADEFFXXX"
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Attributes:
|
|
139
|
+
|
|
140
|
+
- `iban`
|
|
141
|
+
Example: `DE89370400440532013000`
|
|
142
|
+
Must be a syntactically valid IBAN. Spaces are removed automatically.
|
|
143
|
+
- `bic`
|
|
144
|
+
Example: `COBADEFFXXX`
|
|
145
|
+
Optional. If omitted, the serializer falls back to `NOTPROVIDED` in the XML.
|
|
146
|
+
|
|
147
|
+
Useful methods:
|
|
148
|
+
|
|
149
|
+
- `bic_or_placeholder`
|
|
150
|
+
Returns the actual BIC if present, otherwise `NOTPROVIDED`.
|
|
151
|
+
|
|
152
|
+
### `Brot::Pain00100112::Transfer`
|
|
153
|
+
|
|
154
|
+
Use `Transfer` for each outgoing credit transfer inside the payment batch.
|
|
155
|
+
|
|
156
|
+
Example:
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
transfer = Brot::Pain00100112::Transfer.new(
|
|
160
|
+
amount: '1250.50',
|
|
161
|
+
creditor_name: 'Example Supplier GmbH',
|
|
162
|
+
creditor_iban: 'DE89370400440532013000',
|
|
163
|
+
creditor_bic: 'COBADEFFXXX',
|
|
164
|
+
end_to_end_id: 'INV-2026-0001',
|
|
165
|
+
remittance_information: 'Invoice 2026-0001',
|
|
166
|
+
instruction_id: 'PAY-0001',
|
|
167
|
+
purpose_code: 'SUPP'
|
|
168
|
+
)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Initialization attributes:
|
|
172
|
+
|
|
173
|
+
- `amount`
|
|
174
|
+
Example: `'1250.50'`
|
|
175
|
+
Required. Must be positive and have at most two decimal places.
|
|
176
|
+
- `creditor_name`
|
|
177
|
+
Example: `'Example Supplier GmbH'`
|
|
178
|
+
Required. Maximum length is 70 characters in this implementation.
|
|
179
|
+
- `creditor_iban`
|
|
180
|
+
Example: `'DE89370400440532013000'`
|
|
181
|
+
Required. Validated as an IBAN.
|
|
182
|
+
- `creditor_bic`
|
|
183
|
+
Example: `'COBADEFFXXX'`
|
|
184
|
+
Optional. Validated as a BIC if supplied.
|
|
185
|
+
- `end_to_end_id`
|
|
186
|
+
Example: `'INV-2026-0001'`
|
|
187
|
+
Required. Maximum length is 35 characters.
|
|
188
|
+
- `remittance_information`
|
|
189
|
+
Example: `'Invoice 2026-0001'`
|
|
190
|
+
Required. Maximum length is 140 characters.
|
|
191
|
+
- `instruction_id`
|
|
192
|
+
Example: `'PAY-0001'`
|
|
193
|
+
Optional. Maximum length is 35 characters.
|
|
194
|
+
- `purpose_code`
|
|
195
|
+
Example: `'SUPP'`
|
|
196
|
+
Optional. Maximum length is 4 characters.
|
|
197
|
+
|
|
198
|
+
Readable attributes after initialization:
|
|
199
|
+
|
|
200
|
+
- `amount`
|
|
201
|
+
- `creditor_name`
|
|
202
|
+
- `creditor_account`
|
|
203
|
+
- `end_to_end_id`
|
|
204
|
+
- `remittance_information`
|
|
205
|
+
- `instruction_id`
|
|
206
|
+
- `purpose_code`
|
|
207
|
+
|
|
208
|
+
### `Brot::Pain00100112::Document`
|
|
209
|
+
|
|
210
|
+
Use `Document` to represent the full payment file. This is the main entry
|
|
211
|
+
point of the gem.
|
|
212
|
+
|
|
213
|
+
Example:
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
document = Brot::Pain00100112::Document.new(
|
|
217
|
+
message_id: 'MSG-20260313-01',
|
|
218
|
+
payment_information_id: 'PMT-20260313-01',
|
|
219
|
+
initiating_party_name: 'Example Debtor GmbH',
|
|
220
|
+
debtor_name: 'Example Debtor GmbH',
|
|
221
|
+
debtor_iban: 'DE12500105170648489890',
|
|
222
|
+
debtor_bic: 'INGDDEFFXXX',
|
|
223
|
+
requested_execution_date: Date.new(2026, 3, 13),
|
|
224
|
+
transfers: [transfer],
|
|
225
|
+
batch_booking: true,
|
|
226
|
+
created_at: Time.utc(2026, 3, 13, 12, 0, 0)
|
|
227
|
+
)
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Initialization attributes:
|
|
231
|
+
|
|
232
|
+
- `message_id`
|
|
233
|
+
Example: `'MSG-20260313-01'`
|
|
234
|
+
Required. Group header message identifier. Maximum length is 35 characters.
|
|
235
|
+
- `payment_information_id`
|
|
236
|
+
Example: `'PMT-20260313-01'`
|
|
237
|
+
Required. Payment information block identifier. Maximum length is 35
|
|
238
|
+
characters.
|
|
239
|
+
- `initiating_party_name`
|
|
240
|
+
Example: `'Example Debtor GmbH'`
|
|
241
|
+
Required. Maximum length is 70 characters.
|
|
242
|
+
- `debtor_name`
|
|
243
|
+
Example: `'Example Debtor GmbH'`
|
|
244
|
+
Required. Maximum length is 70 characters.
|
|
245
|
+
- `debtor_iban`
|
|
246
|
+
Example: `'DE12500105170648489890'`
|
|
247
|
+
Required. Validated as an IBAN.
|
|
248
|
+
- `debtor_bic`
|
|
249
|
+
Example: `'INGDDEFFXXX'`
|
|
250
|
+
Optional. Validated as a BIC if supplied.
|
|
251
|
+
- `requested_execution_date`
|
|
252
|
+
Example: `Date.new(2026, 3, 13)` or `'2026-03-13'`
|
|
253
|
+
Required. Date only.
|
|
254
|
+
- `transfers`
|
|
255
|
+
Example: `[transfer_1, transfer_2]`
|
|
256
|
+
Required. Must contain at least one `Brot::Pain00100112::Transfer`.
|
|
257
|
+
- `batch_booking`
|
|
258
|
+
Example: `true`
|
|
259
|
+
Optional. Defaults to `true`.
|
|
260
|
+
- `created_at`
|
|
261
|
+
Example: `Time.utc(2026, 3, 13, 12, 0, 0)`
|
|
262
|
+
Optional. Defaults to the current UTC time.
|
|
263
|
+
|
|
264
|
+
Readable attributes after initialization:
|
|
265
|
+
|
|
266
|
+
- `message_id`
|
|
267
|
+
- `payment_information_id`
|
|
268
|
+
- `initiating_party_name`
|
|
269
|
+
- `debtor_name`
|
|
270
|
+
- `debtor_account`
|
|
271
|
+
- `requested_execution_date`
|
|
272
|
+
- `transfers`
|
|
273
|
+
- `batch_booking`
|
|
274
|
+
- `created_at`
|
|
275
|
+
|
|
276
|
+
Useful methods:
|
|
277
|
+
|
|
278
|
+
- `to_xml(pretty: true)`
|
|
279
|
+
Serializes the document to `pain.001.001.12` XML.
|
|
280
|
+
- `validate(xsd_path: Brot::Pain00100112::Schema.bundled_xsd_path)`
|
|
281
|
+
Validates the generated XML and returns a `Schema::Result`.
|
|
282
|
+
- `control_sum`
|
|
283
|
+
Returns the sum of all transfer amounts as a `BigDecimal`.
|
|
284
|
+
- `number_of_transactions`
|
|
285
|
+
Returns the number of transfers.
|
|
286
|
+
|
|
287
|
+
### `Brot::Pain00100112::Schema`
|
|
288
|
+
|
|
289
|
+
Use `Schema` when you want to validate XML directly without first building a
|
|
290
|
+
`Document` object.
|
|
291
|
+
|
|
292
|
+
Example:
|
|
293
|
+
|
|
294
|
+
```ruby
|
|
295
|
+
xml = document.to_xml(pretty: false)
|
|
296
|
+
result = Brot::Pain00100112::Schema.validate(xml)
|
|
297
|
+
|
|
298
|
+
raise result.errors.join("\n") unless result.valid?
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Available methods:
|
|
302
|
+
|
|
303
|
+
- `bundled_xsd_path`
|
|
304
|
+
Returns the absolute path of the XSD bundled inside the gem.
|
|
305
|
+
- `validate(xml, xsd_path: bundled_xsd_path)`
|
|
306
|
+
Validates an XML string against the bundled schema by default.
|
|
307
|
+
|
|
308
|
+
### `Brot::Pain00100112::Schema::Result`
|
|
309
|
+
|
|
310
|
+
`Schema.validate` and `Document#validate` both return a
|
|
311
|
+
`Brot::Pain00100112::Schema::Result`.
|
|
312
|
+
|
|
313
|
+
Example:
|
|
314
|
+
|
|
315
|
+
```ruby
|
|
316
|
+
result = document.validate
|
|
317
|
+
|
|
318
|
+
result.valid?
|
|
319
|
+
# => true
|
|
320
|
+
|
|
321
|
+
result.errors
|
|
322
|
+
# => []
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Attributes and methods:
|
|
326
|
+
|
|
327
|
+
- `errors`
|
|
328
|
+
An array of schema validation error messages.
|
|
329
|
+
- `valid?`
|
|
330
|
+
Returns `true` when `errors` is empty.
|
|
331
|
+
|
|
332
|
+
## End-To-End Example
|
|
333
|
+
|
|
334
|
+
This example shows the normal workflow from transfers to XML generation and
|
|
335
|
+
validation:
|
|
336
|
+
|
|
337
|
+
```ruby
|
|
338
|
+
require 'brot'
|
|
339
|
+
require 'date'
|
|
340
|
+
|
|
341
|
+
transfers = [
|
|
342
|
+
Brot::Pain00100112::Transfer.new(
|
|
343
|
+
amount: '1250.50',
|
|
344
|
+
creditor_name: 'Example Supplier GmbH',
|
|
345
|
+
creditor_iban: 'DE89370400440532013000',
|
|
346
|
+
creditor_bic: 'COBADEFFXXX',
|
|
347
|
+
end_to_end_id: 'INV-2026-0001',
|
|
348
|
+
remittance_information: 'Invoice 2026-0001'
|
|
349
|
+
),
|
|
350
|
+
Brot::Pain00100112::Transfer.new(
|
|
351
|
+
amount: '349.99',
|
|
352
|
+
creditor_name: 'Another Supplier GmbH',
|
|
353
|
+
creditor_iban: 'DE44500105175407324931',
|
|
354
|
+
end_to_end_id: 'INV-2026-0002',
|
|
355
|
+
remittance_information: 'Invoice 2026-0002'
|
|
356
|
+
)
|
|
357
|
+
]
|
|
358
|
+
|
|
359
|
+
document = Brot::Pain00100112::Document.new(
|
|
360
|
+
message_id: 'MSG-20260313-01',
|
|
361
|
+
payment_information_id: 'PMT-20260313-01',
|
|
362
|
+
initiating_party_name: 'Example Debtor GmbH',
|
|
363
|
+
debtor_name: 'Example Debtor GmbH',
|
|
364
|
+
debtor_iban: 'DE12500105170648489890',
|
|
365
|
+
debtor_bic: 'INGDDEFFXXX',
|
|
366
|
+
requested_execution_date: Date.new(2026, 3, 13),
|
|
367
|
+
transfers: transfers
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
xml = document.to_xml
|
|
371
|
+
result = document.validate
|
|
372
|
+
|
|
373
|
+
abort(result.errors.join("\n")) unless result.valid?
|
|
374
|
+
|
|
375
|
+
puts xml
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
## Validation
|
|
379
|
+
|
|
380
|
+
The gem ships with the `pain.001.001.12.xsd` file, so validation works without
|
|
381
|
+
an external path:
|
|
382
|
+
|
|
383
|
+
```ruby
|
|
384
|
+
result = document.validate
|
|
385
|
+
|
|
386
|
+
raise result.errors.join("\n") unless result.valid?
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
If you want to override the schema file, pass `xsd_path:` explicitly.
|
|
390
|
+
|
|
391
|
+
## Error Handling
|
|
392
|
+
|
|
393
|
+
Invalid input values raise `Brot::Pain00100112::ValidationError`.
|
|
394
|
+
|
|
395
|
+
Typical examples:
|
|
396
|
+
|
|
397
|
+
- Invalid IBAN
|
|
398
|
+
- Invalid BIC
|
|
399
|
+
- Empty required text fields
|
|
400
|
+
- Amounts that are zero, negative, or have too many decimal places
|
|
401
|
+
- Missing transfers
|
|
402
|
+
|
|
403
|
+
Example:
|
|
404
|
+
|
|
405
|
+
```ruby
|
|
406
|
+
begin
|
|
407
|
+
Brot::Pain00100112::Transfer.new(
|
|
408
|
+
amount: '0',
|
|
409
|
+
creditor_name: 'Broken Example',
|
|
410
|
+
creditor_iban: 'INVALID',
|
|
411
|
+
end_to_end_id: 'X',
|
|
412
|
+
remittance_information: 'Broken'
|
|
413
|
+
)
|
|
414
|
+
rescue Brot::Pain00100112::ValidationError => error
|
|
415
|
+
warn error.message
|
|
416
|
+
end
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
## Development
|
|
420
|
+
|
|
421
|
+
```sh
|
|
422
|
+
bundle install
|
|
423
|
+
bundle exec rake
|
|
424
|
+
```
|
data/Rakefile
ADDED
data/brot.gemspec
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/brot/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'brot'
|
|
7
|
+
spec.version = Brot::VERSION
|
|
8
|
+
spec.authors = ['Vincent Garrigues']
|
|
9
|
+
spec.email = ['vincent@garriguv.io']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'Build ISO 20022 pain.001 payment initiation XML for DATEV uploads.'
|
|
12
|
+
spec.description = <<~TEXT
|
|
13
|
+
brot builds SEPA credit transfer XML with Nokogiri::XML::Builder.
|
|
14
|
+
The output targets pain.001.001.12 for DATEV-oriented uploads.
|
|
15
|
+
TEXT
|
|
16
|
+
spec.homepage = 'https://github.com/garriguv/brot'
|
|
17
|
+
spec.license = 'MIT'
|
|
18
|
+
spec.required_ruby_version = '>= 4.0.0'
|
|
19
|
+
|
|
20
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
21
|
+
spec.metadata['source_code_uri'] = spec.homepage
|
|
22
|
+
spec.metadata['changelog_uri'] = "#{spec.homepage}/releases"
|
|
23
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
24
|
+
|
|
25
|
+
spec.files = Dir.glob(
|
|
26
|
+
[
|
|
27
|
+
'.github/workflows/*.yml',
|
|
28
|
+
'.rubocop.yml',
|
|
29
|
+
'Gemfile',
|
|
30
|
+
'LICENSE.txt',
|
|
31
|
+
'README.md',
|
|
32
|
+
'Rakefile',
|
|
33
|
+
'brot.gemspec',
|
|
34
|
+
'lib/**/*.rb',
|
|
35
|
+
'test/**/*.rb',
|
|
36
|
+
'xsd/**/*.xsd'
|
|
37
|
+
]
|
|
38
|
+
)
|
|
39
|
+
spec.bindir = 'exe'
|
|
40
|
+
spec.executables = []
|
|
41
|
+
spec.require_paths = ['lib']
|
|
42
|
+
|
|
43
|
+
spec.add_dependency 'nokogiri', '~> 1.18'
|
|
44
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Brot
|
|
4
|
+
module Pain00100112
|
|
5
|
+
# Bank account details used in debtor and creditor sections.
|
|
6
|
+
#
|
|
7
|
+
# You will usually not instantiate {Account} directly because {Document}
|
|
8
|
+
# and {Transfer} build account objects internally from `*_iban` and
|
|
9
|
+
# `*_bic` keyword arguments.
|
|
10
|
+
#
|
|
11
|
+
# @example Build an account manually
|
|
12
|
+
# account = Brot::Pain00100112::Account.new(
|
|
13
|
+
# iban: 'DE89370400440532013000',
|
|
14
|
+
# bic: 'COBADEFFXXX'
|
|
15
|
+
# )
|
|
16
|
+
#
|
|
17
|
+
# account.iban
|
|
18
|
+
# # => "DE89370400440532013000"
|
|
19
|
+
#
|
|
20
|
+
# @attr_reader iban
|
|
21
|
+
# The normalized IBAN.
|
|
22
|
+
# Example: `DE89370400440532013000`
|
|
23
|
+
# @attr_reader bic
|
|
24
|
+
# The normalized BIC, or `nil` if none was provided.
|
|
25
|
+
# Example: `COBADEFFXXX`
|
|
26
|
+
class Account
|
|
27
|
+
attr_reader :bic, :iban
|
|
28
|
+
|
|
29
|
+
# @param iban [String]
|
|
30
|
+
# Required IBAN.
|
|
31
|
+
# Example: `DE89370400440532013000`
|
|
32
|
+
# @param bic [String, nil]
|
|
33
|
+
# Optional BIC.
|
|
34
|
+
# Example: `COBADEFFXXX`
|
|
35
|
+
def initialize(iban:, bic: nil)
|
|
36
|
+
@iban = Utils.normalize_iban!(iban)
|
|
37
|
+
@bic = bic.nil? ? nil : Utils.normalize_bic!(bic)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Returns the BIC if present, otherwise `NOTPROVIDED`.
|
|
41
|
+
#
|
|
42
|
+
# @return [String]
|
|
43
|
+
def bic_or_placeholder
|
|
44
|
+
bic || Utils.default_financial_institution_id
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|