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.
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Brot
4
+ module Pain00100112
5
+ # Payment batch document that serializes into pain.001.001.12 XML.
6
+ #
7
+ # This is the main entry point of the gem. A document corresponds to one
8
+ # complete XML file containing one debtor and one or more outgoing credit
9
+ # transfers.
10
+ #
11
+ # @example Build and validate a payment file
12
+ # transfer = Brot::Pain00100112::Transfer.new(
13
+ # amount: '1250.50',
14
+ # creditor_name: 'Example Supplier GmbH',
15
+ # creditor_iban: 'DE89370400440532013000',
16
+ # end_to_end_id: 'INV-2026-0001',
17
+ # remittance_information: 'Invoice 2026-0001'
18
+ # )
19
+ #
20
+ # document = Brot::Pain00100112::Document.new(
21
+ # message_id: 'MSG-20260313-01',
22
+ # payment_information_id: 'PMT-20260313-01',
23
+ # initiating_party_name: 'Example Debtor GmbH',
24
+ # debtor_name: 'Example Debtor GmbH',
25
+ # debtor_iban: 'DE12500105170648489890',
26
+ # requested_execution_date: Date.new(2026, 3, 13),
27
+ # transfers: [transfer]
28
+ # )
29
+ #
30
+ # result = document.validate
31
+ # raise result.errors.join("\n") unless result.valid?
32
+ #
33
+ # @attr_reader message_id
34
+ # Group header message identifier.
35
+ # Example: `MSG-20260313-01`
36
+ # @attr_reader payment_information_id
37
+ # Payment information block identifier.
38
+ # Example: `PMT-20260313-01`
39
+ # @attr_reader initiating_party_name
40
+ # Name written into `GrpHdr/InitgPty/Nm`.
41
+ # Example: `Example Debtor GmbH`
42
+ # @attr_reader debtor_name
43
+ # Name written into `PmtInf/Dbtr/Nm`.
44
+ # Example: `Example Debtor GmbH`
45
+ # @attr_reader debtor_account
46
+ # Debtor bank account as an {Account}.
47
+ # @attr_reader requested_execution_date
48
+ # Requested execution date as a {Date}.
49
+ # @attr_reader transfers
50
+ # Array of {Transfer} objects.
51
+ # @attr_reader batch_booking
52
+ # Whether the file asks for batch booking.
53
+ # @attr_reader created_at
54
+ # Creation time written into `GrpHdr/CreDtTm`.
55
+ class Document
56
+ attr_reader :batch_booking,
57
+ :created_at,
58
+ :debtor_account,
59
+ :debtor_name,
60
+ :initiating_party_name,
61
+ :message_id,
62
+ :payment_information_id,
63
+ :requested_execution_date,
64
+ :transfers
65
+
66
+ # @param attributes [Hash]
67
+ # @option attributes [String] :message_id
68
+ # Example: `MSG-20260313-01`
69
+ # @option attributes [String] :payment_information_id
70
+ # Example: `PMT-20260313-01`
71
+ # @option attributes [String] :initiating_party_name
72
+ # Example: `Example Debtor GmbH`
73
+ # @option attributes [String] :debtor_name
74
+ # Example: `Example Debtor GmbH`
75
+ # @option attributes [String] :debtor_iban
76
+ # Example: `DE12500105170648489890`
77
+ # @option attributes [String, nil] :debtor_bic
78
+ # Example: `INGDDEFFXXX`
79
+ # @option attributes [Date, String] :requested_execution_date
80
+ # Example: `Date.new(2026, 3, 13)` or `'2026-03-13'`
81
+ # @option attributes [Array<Transfer>] :transfers
82
+ # Must contain at least one transfer.
83
+ # @option attributes [Boolean] :batch_booking
84
+ # Defaults to `true`.
85
+ # @option attributes [Time, String] :created_at
86
+ # Defaults to the current UTC time.
87
+ def initialize(**attributes)
88
+ assign_identifiers(attributes)
89
+ assign_party_details(attributes)
90
+ assign_payment_schedule(attributes)
91
+ end
92
+
93
+ # Returns the sum of all transfer amounts.
94
+ #
95
+ # @return [BigDecimal]
96
+ def control_sum
97
+ transfers.sum(BigDecimal('0'), &:amount)
98
+ end
99
+
100
+ # Returns the number of transfers in the document.
101
+ #
102
+ # @return [Integer]
103
+ def number_of_transactions
104
+ transfers.length
105
+ end
106
+
107
+ # Serializes the document into `pain.001.001.12` XML.
108
+ #
109
+ # @param pretty [Boolean]
110
+ # When `true`, outputs formatted XML with indentation.
111
+ # @return [String]
112
+ def to_xml(pretty: true)
113
+ Serializer.new(self).to_xml(pretty: pretty)
114
+ end
115
+
116
+ # Validates the generated XML against the bundled schema by default.
117
+ #
118
+ # @param xsd_path [String]
119
+ # Optional custom XSD path.
120
+ # @return [Schema::Result]
121
+ def validate(xsd_path: Schema.bundled_xsd_path)
122
+ Schema.validate(to_xml(pretty: false), xsd_path: xsd_path)
123
+ end
124
+
125
+ private
126
+
127
+ def assign_identifiers(attributes)
128
+ @message_id = Utils.normalize_text!('message_id', attributes.fetch(:message_id), max: 35)
129
+ @payment_information_id = Utils.normalize_text!(
130
+ 'payment_information_id',
131
+ attributes.fetch(:payment_information_id),
132
+ max: 35
133
+ )
134
+ end
135
+
136
+ def assign_party_details(attributes)
137
+ @initiating_party_name = Utils.normalize_text!(
138
+ 'initiating_party_name',
139
+ attributes.fetch(:initiating_party_name),
140
+ max: 70
141
+ )
142
+ @debtor_name = Utils.normalize_text!('debtor_name', attributes.fetch(:debtor_name), max: 70)
143
+ @debtor_account = Account.new(iban: attributes.fetch(:debtor_iban), bic: attributes[:debtor_bic])
144
+ end
145
+
146
+ def assign_payment_schedule(attributes)
147
+ @requested_execution_date = Utils.normalize_date!(attributes.fetch(:requested_execution_date))
148
+ @transfers = normalize_transfers!(attributes.fetch(:transfers))
149
+ @batch_booking = attributes.fetch(:batch_booking, true) ? true : false
150
+ @created_at = Utils.normalize_time!(attributes.fetch(:created_at, Time.now.utc))
151
+ end
152
+
153
+ def normalize_transfers!(transfers)
154
+ value = Array(transfers)
155
+ raise ValidationError, 'at least one transfer is required' if value.empty?
156
+ raise ValidationError, 'transfers must all be Transfer instances' unless value.all?(Transfer)
157
+
158
+ value
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Brot
4
+ module Pain00100112
5
+ # XML schema validation helpers for generated payment files.
6
+ #
7
+ # Use this module when you already have an XML string and want to validate
8
+ # it against the bundled `pain.001.001.12.xsd`.
9
+ #
10
+ # @example Validate XML directly
11
+ # xml = document.to_xml(pretty: false)
12
+ # result = Brot::Pain00100112::Schema.validate(xml)
13
+ #
14
+ # raise result.errors.join("\n") unless result.valid?
15
+ module Schema
16
+ # Result returned by schema validation.
17
+ #
18
+ # @example Check a validation result
19
+ # result = Brot::Pain00100112::Schema.validate(xml)
20
+ #
21
+ # result.valid?
22
+ # # => true
23
+ #
24
+ # result.errors
25
+ # # => []
26
+ Result = Data.define(:errors) do
27
+ # @return [Boolean]
28
+ def valid?
29
+ errors.empty?
30
+ end
31
+ end
32
+
33
+ module_function
34
+
35
+ # Absolute path to the XSD file bundled inside the gem.
36
+ #
37
+ # @return [String]
38
+ def bundled_xsd_path
39
+ File.expand_path('../../../xsd/pain.001.001.12.xsd', __dir__)
40
+ end
41
+
42
+ # Validates XML against the bundled schema by default.
43
+ #
44
+ # @param xml [String]
45
+ # A `pain.001.001.12` XML document.
46
+ # @param xsd_path [String]
47
+ # Optional custom XSD path.
48
+ # @return [Result]
49
+ def validate(xml, xsd_path: bundled_xsd_path)
50
+ schema = Nokogiri::XML::Schema(File.read(xsd_path))
51
+ document = Nokogiri::XML(xml, &:strict)
52
+ errors = schema.validate(document).map { it.message.strip }
53
+
54
+ Result.new(errors: errors)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Brot
4
+ module Pain00100112
5
+ # Concrete serializer for pain.001.001.12 output.
6
+ #
7
+ # Most callers should use {Document#to_xml} instead of instantiating this
8
+ # class directly.
9
+ class Serializer < SerializerBase
10
+ private
11
+
12
+ def bic_element_name
13
+ :BICFI
14
+ end
15
+
16
+ def namespace
17
+ NAMESPACE
18
+ end
19
+
20
+ def requested_execution_date(xml)
21
+ xml.ReqdExctnDt { xml.Dt(document.requested_execution_date.iso8601) }
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Brot
4
+ module Pain00100112
5
+ # Helper methods shared by serializer implementations.
6
+ module SerializerWritingHelpers
7
+ GROUP_HEADER_FIELDS = [
8
+ [:MsgId, :message_id.to_proc],
9
+ [:CreDtTm, ->(doc) { doc.created_at.iso8601 }],
10
+ [:NbOfTxs, ->(doc) { doc.number_of_transactions.to_s }],
11
+ [:CtrlSum, ->(doc) { Utils.format_amount(doc.control_sum) }]
12
+ ].freeze
13
+
14
+ PAYMENT_INFORMATION_FIELDS = [
15
+ [:PmtInfId, :payment_information_id.to_proc],
16
+ [:PmtMtd, ->(_) { 'TRF' }],
17
+ [:BtchBookg, :batch_booking.to_proc],
18
+ [:NbOfTxs, ->(doc) { doc.number_of_transactions.to_s }],
19
+ [:CtrlSum, ->(doc) { Utils.format_amount(doc.control_sum) }]
20
+ ].freeze
21
+
22
+ private
23
+
24
+ def named_party(xml, element_name, name)
25
+ xml.public_send(element_name) { xml.Nm(name) }
26
+ end
27
+
28
+ def payment_header(xml)
29
+ write_fields(xml, PAYMENT_INFORMATION_FIELDS)
30
+ payment_type_information(xml)
31
+ requested_execution_date(xml)
32
+ end
33
+
34
+ def debtor_details(xml)
35
+ named_party(xml, :Dbtr, document.debtor_name)
36
+ debtor_account(xml)
37
+ agent(xml, :DbtrAgt, document.debtor_account)
38
+ xml.ChrgBr('SLEV')
39
+ end
40
+
41
+ def transfer_details(xml)
42
+ document.transfers.each { build_credit_transfer(xml, it) }
43
+ end
44
+
45
+ def write_fields(xml, fields)
46
+ fields.each do |element_name, extractor|
47
+ xml.public_send(element_name, extractor.call(document))
48
+ end
49
+ end
50
+ end
51
+
52
+ # Shared XML writer helpers for payment initiation serializers.
53
+ #
54
+ # This is an internal base class. For normal application code, use
55
+ # {Document#to_xml} rather than calling serializer classes directly.
56
+ class SerializerBase
57
+ include SerializerWritingHelpers
58
+
59
+ # @param document [Document]
60
+ def initialize(document)
61
+ @document = document
62
+ end
63
+
64
+ # @param pretty [Boolean]
65
+ # @return [String]
66
+ def to_xml(pretty: true)
67
+ builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
68
+ build_document(xml)
69
+ end
70
+
71
+ return builder.to_xml if pretty
72
+
73
+ builder.doc.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML)
74
+ end
75
+
76
+ private
77
+
78
+ attr_reader :document
79
+
80
+ def build_credit_transfer(xml, transfer)
81
+ xml.CdtTrfTxInf do
82
+ payment_identification(xml, transfer)
83
+ amount(xml, transfer.amount)
84
+ agent(xml, :CdtrAgt, transfer.creditor_account)
85
+ creditor(xml, transfer)
86
+ purpose(xml, transfer.purpose_code)
87
+ remittance_information(xml, transfer.remittance_information)
88
+ end
89
+ end
90
+
91
+ def build_document(xml)
92
+ xml.Document(xmlns: namespace) do
93
+ xml.CstmrCdtTrfInitn do
94
+ group_header(xml)
95
+ payment_information(xml)
96
+ end
97
+ end
98
+ end
99
+
100
+ def group_header(xml)
101
+ xml.GrpHdr do
102
+ write_fields(xml, GROUP_HEADER_FIELDS)
103
+ named_party(xml, :InitgPty, document.initiating_party_name)
104
+ end
105
+ end
106
+
107
+ def payment_information(xml)
108
+ xml.PmtInf do
109
+ payment_header(xml)
110
+ debtor_details(xml)
111
+ transfer_details(xml)
112
+ end
113
+ end
114
+
115
+ def payment_type_information(xml)
116
+ xml.PmtTpInf do
117
+ xml.SvcLvl { xml.Cd('SEPA') }
118
+ end
119
+ end
120
+
121
+ def payment_identification(xml, transfer)
122
+ xml.PmtId do
123
+ xml.InstrId(transfer.instruction_id) if transfer.instruction_id
124
+ xml.EndToEndId(transfer.end_to_end_id)
125
+ end
126
+ end
127
+
128
+ def amount(xml, amount)
129
+ xml.Amt do
130
+ xml.InstdAmt(Utils.format_amount(amount), Ccy: 'EUR')
131
+ end
132
+ end
133
+
134
+ def creditor(xml, transfer)
135
+ named_party(xml, :Cdtr, transfer.creditor_name)
136
+ xml.CdtrAcct { account_identification(xml, transfer.creditor_account) }
137
+ end
138
+
139
+ def debtor_account(xml)
140
+ xml.DbtrAcct { account_identification(xml, document.debtor_account) }
141
+ end
142
+
143
+ def account_identification(xml, account)
144
+ xml.Id { xml.IBAN(account.iban) }
145
+ end
146
+
147
+ def agent(xml, element_name, account)
148
+ xml.public_send(element_name) do
149
+ xml.FinInstnId do
150
+ if account.bic
151
+ xml.public_send(bic_element_name, account.bic)
152
+ else
153
+ other_financial_institution_id(xml)
154
+ end
155
+ end
156
+ end
157
+ end
158
+
159
+ def other_financial_institution_id(xml)
160
+ xml.Othr { xml.Id(Utils.default_financial_institution_id) }
161
+ end
162
+
163
+ def purpose(xml, purpose_code)
164
+ return unless purpose_code
165
+
166
+ xml.Purp { xml.Cd(purpose_code) }
167
+ end
168
+
169
+ def remittance_information(xml, value)
170
+ xml.RmtInf { xml.Ustrd(value) }
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Brot
4
+ module Pain00100112
5
+ # Single credit transfer entry inside a payment batch.
6
+ #
7
+ # Each instance becomes one `CdtTrfTxInf` block inside the output XML.
8
+ #
9
+ # @example Create a transfer
10
+ # transfer = Brot::Pain00100112::Transfer.new(
11
+ # amount: '1250.50',
12
+ # creditor_name: 'Example Supplier GmbH',
13
+ # creditor_iban: 'DE89370400440532013000',
14
+ # creditor_bic: 'COBADEFFXXX',
15
+ # end_to_end_id: 'INV-2026-0001',
16
+ # remittance_information: 'Invoice 2026-0001',
17
+ # instruction_id: 'PAY-0001',
18
+ # purpose_code: 'SUPP'
19
+ # )
20
+ #
21
+ # @attr_reader amount
22
+ # Transfer amount as a `BigDecimal`.
23
+ # Example input: `'1250.50'`
24
+ # @attr_reader creditor_name
25
+ # Name written into `Cdtr/Nm`.
26
+ # Example: `Example Supplier GmbH`
27
+ # @attr_reader creditor_account
28
+ # Creditor bank account as an {Account}.
29
+ # @attr_reader end_to_end_id
30
+ # End-to-end reference.
31
+ # Example: `INV-2026-0001`
32
+ # @attr_reader remittance_information
33
+ # Unstructured remittance information.
34
+ # Example: `Invoice 2026-0001`
35
+ # @attr_reader instruction_id
36
+ # Optional instruction identifier.
37
+ # Example: `PAY-0001`
38
+ # @attr_reader purpose_code
39
+ # Optional purpose code.
40
+ # Example: `SUPP`
41
+ class Transfer
42
+ attr_reader :amount,
43
+ :creditor_account,
44
+ :creditor_name,
45
+ :end_to_end_id,
46
+ :instruction_id,
47
+ :purpose_code,
48
+ :remittance_information
49
+
50
+ # @param attributes [Hash]
51
+ # @option attributes [String, Numeric] :amount
52
+ # Example: `'1250.50'`
53
+ # @option attributes [String] :creditor_name
54
+ # Example: `Example Supplier GmbH`
55
+ # @option attributes [String] :creditor_iban
56
+ # Example: `DE89370400440532013000`
57
+ # @option attributes [String, nil] :creditor_bic
58
+ # Example: `COBADEFFXXX`
59
+ # @option attributes [String] :end_to_end_id
60
+ # Example: `INV-2026-0001`
61
+ # @option attributes [String] :remittance_information
62
+ # Example: `Invoice 2026-0001`
63
+ # @option attributes [String, nil] :instruction_id
64
+ # Example: `PAY-0001`
65
+ # @option attributes [String, nil] :purpose_code
66
+ # Example: `SUPP`
67
+ def initialize(**attributes)
68
+ assign_creditor_details(attributes)
69
+ assign_payment_references(attributes)
70
+ end
71
+
72
+ private
73
+
74
+ def assign_creditor_details(attributes)
75
+ @amount = Utils.normalize_amount!(attributes.fetch(:amount))
76
+ @creditor_name = Utils.normalize_text!('creditor_name', attributes.fetch(:creditor_name), max: 70)
77
+ @creditor_account = Account.new(iban: attributes.fetch(:creditor_iban), bic: attributes[:creditor_bic])
78
+ end
79
+
80
+ def assign_payment_references(attributes)
81
+ @end_to_end_id = Utils.normalize_text!('end_to_end_id', attributes.fetch(:end_to_end_id), max: 35)
82
+ @remittance_information = Utils.normalize_text!(
83
+ 'remittance_information',
84
+ attributes.fetch(:remittance_information),
85
+ max: 140
86
+ )
87
+ @instruction_id = normalize_optional_text(attributes[:instruction_id], 'instruction_id', 35)
88
+ @purpose_code = normalize_optional_text(attributes[:purpose_code], 'purpose_code', 4)
89
+ end
90
+
91
+ def normalize_optional_text(value, name, max)
92
+ value.nil? ? nil : Utils.normalize_text!(name, value, max: max)
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Brot
4
+ module Pain00100112
5
+ # Normalization and validation helpers for payment data.
6
+ #
7
+ # This module is internal. Application code should normally use the public
8
+ # classes {Account}, {Transfer}, {Document}, and {Schema}.
9
+ module Utils
10
+ module_function
11
+
12
+ def normalize_text!(name, value, max:)
13
+ text = String(value).strip
14
+ raise ValidationError, "#{name} must not be empty" if text.empty?
15
+ raise ValidationError, "#{name} exceeds #{max} characters" if text.length > max
16
+
17
+ text
18
+ end
19
+
20
+ def normalize_iban!(iban)
21
+ value = String(iban).upcase.delete(' ')
22
+ raise ValidationError, 'iban is invalid' unless valid_iban?(value)
23
+
24
+ value
25
+ end
26
+
27
+ def normalize_bic!(bic)
28
+ value = String(bic).upcase.strip
29
+ unless /\A[A-Z0-9]{4}[A-Z]{2}[A-Z0-9]{2}(?:[A-Z0-9]{3})?\z/.match?(value)
30
+ raise ValidationError,
31
+ 'bic is invalid'
32
+ end
33
+
34
+ value
35
+ end
36
+
37
+ def normalize_amount!(amount)
38
+ value = BigDecimal(amount.to_s)
39
+ raise ValidationError, 'amount must be positive' unless value.positive?
40
+ raise ValidationError, 'amount must have at most two decimal places' unless two_decimal_places?(value)
41
+
42
+ value
43
+ rescue ArgumentError
44
+ raise ValidationError, 'amount is invalid'
45
+ end
46
+
47
+ def normalize_date!(date)
48
+ return date if date.is_a?(Date)
49
+ return Date.iso8601(date) if date.is_a?(String)
50
+
51
+ raise ValidationError, 'requested_execution_date must be a Date or ISO 8601 string'
52
+ end
53
+
54
+ def normalize_time!(value)
55
+ return value.getutc if value.respond_to?(:getutc)
56
+ return Time.iso8601(value).getutc if value.is_a?(String)
57
+
58
+ raise ValidationError, 'created_at must be a Time-like object or ISO 8601 string'
59
+ end
60
+
61
+ def format_amount(amount)
62
+ Kernel.format('%.2f', amount)
63
+ end
64
+
65
+ def default_financial_institution_id
66
+ 'NOTPROVIDED'
67
+ end
68
+
69
+ def valid_iban?(value)
70
+ return false unless /\A[A-Z]{2}\d{2}[A-Z0-9]{11,30}\z/.match?(value)
71
+
72
+ iban_mod97(value) == 1
73
+ end
74
+
75
+ def iban_mod97(iban)
76
+ rearranged = "#{iban[4..]}#{iban[0, 4]}"
77
+ remainder = 0
78
+
79
+ rearranged.each_char do |char|
80
+ chunk = "#{remainder}#{char.ord >= 65 ? char.ord - 55 : char}"
81
+ remainder = chunk.to_i % 97
82
+ end
83
+
84
+ remainder
85
+ end
86
+
87
+ def two_decimal_places?(value)
88
+ ((value * 100) % 1).zero?
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Brot
4
+ module Pain00100112
5
+ # Public namespace for the pain.001.001.12 implementation.
6
+ #
7
+ # Main entry points:
8
+ #
9
+ # - {Document} for building complete payment files
10
+ # - {Transfer} for individual outgoing payments
11
+ # - {Schema} for validating XML against the bundled XSD
12
+ NAMESPACE = 'urn:iso:std:iso:20022:tech:xsd:pain.001.001.12'
13
+
14
+ # Raised when input data cannot be normalized into a valid payment file.
15
+ class ValidationError < Brot::Error; end
16
+ end
17
+ end
18
+
19
+ require_relative 'pain00100112/account'
20
+ require_relative 'pain00100112/document'
21
+ require_relative 'pain00100112/schema'
22
+ require_relative 'pain00100112/serializer_base'
23
+ require_relative 'pain00100112/serializer'
24
+ require_relative 'pain00100112/transfer'
25
+ require_relative 'pain00100112/utils'
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Brot
4
+ VERSION = '0.1.1'
5
+ end
data/lib/brot.rb ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bigdecimal'
4
+ require 'date'
5
+ require 'nokogiri'
6
+ require 'time'
7
+
8
+ module Brot
9
+ class Error < StandardError; end
10
+ end
11
+
12
+ require_relative 'brot/version'
13
+ require_relative 'brot/pain00100112'
data/test/brot_test.rb ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'test_helper'
4
+
5
+ class BrotTest < Minitest::Test
6
+ def test_has_a_version_number
7
+ refute_nil Brot::VERSION
8
+ end
9
+ end