Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
timeout-minutes: 5
strategy:
matrix:
ruby-version: ['2.7', '3.0', '3.1', '3.2', '3.3']
ruby-version: ['3.0', '3.1', '3.2', '3.3', '3.4']

steps:
- uses: actions/checkout@v2
Expand Down
2 changes: 1 addition & 1 deletion .ruby-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.0.2
3.4.3
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
## 0.2.0
## 1.0.0

- support Ruby 2.7+
- Drop bank-contact, use iban-tools (@Joerg-Seitz)
- Inline BIC validation from bank-contact
- Support Ruby 3.4
29 changes: 14 additions & 15 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,28 +1,30 @@
PATH
remote: .
specs:
girocode (0.2.0)
bank-contact
girocode (1.0.0)
bigdecimal
iban-tools
rqrcode

GEM
remote: https://rubygems.org/
specs:
bank-contact (0.0.6)
bigdecimal (3.2.1)
chunky_png (1.4.0)
docile (1.4.0)
minitest (5.14.4)
rake (13.0.6)
rqrcode (2.1.0)
docile (1.4.1)
iban-tools (1.2.1)
minitest (5.25.5)
rake (13.3.0)
rqrcode (3.1.0)
chunky_png (~> 1.0)
rqrcode_core (~> 1.0)
rqrcode_core (1.2.0)
simplecov (0.21.2)
rqrcode_core (~> 2.0)
rqrcode_core (2.0.0)
simplecov (0.22.0)
docile (~> 1.1)
simplecov-html (~> 0.11)
simplecov_json_formatter (~> 0.1)
simplecov-html (0.12.3)
simplecov_json_formatter (0.1.3)
simplecov-html (0.13.1)
simplecov_json_formatter (0.1.4)

PLATFORMS
ruby
Expand All @@ -32,6 +34,3 @@ DEPENDENCIES
minitest
rake
simplecov

BUNDLED WITH
2.2.22
3 changes: 2 additions & 1 deletion girocode.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Gem::Specification.new do |s|
s.files = Dir['{lib}/**/*.rb', 'LICENSE', 'README.md', 'CHANGELOG.md']
s.require_paths = ['lib']

s.add_dependency 'bigdecimal'
s.add_dependency 'rqrcode'
s.add_dependency 'bank-contact'
s.add_dependency 'iban-tools'
end
3 changes: 2 additions & 1 deletion lib/girocode.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
require 'bigdecimal'
require 'bank/contact'
require 'iban-tools'
require 'rqrcode'

require_relative 'girocode/version'
require_relative 'girocode/bic'
require_relative 'girocode/code'

module Girocode
Expand Down
67 changes: 67 additions & 0 deletions lib/girocode/bic.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# derived from bank-contact/bic by Kevin

module Girocode
class BIC
REGEX = /\A([A-Z]{4})([A-Z]{2})([A-Z0-9]{2})([A-Z0-9]{3})?\z/.freeze

def self.valid?(code)
new(code).valid?
end

def initialize(code)
@code = code.to_s.gsub(/\s+/, '').upcase
end

def bank_code
@code[0..3]
end

def country_code
@code[4..5]
end

def location_code
@code[6..7]
end

def branch_code
@code[8..10]
end

def to_s(formatted = false)
formatted ? to_formatted_str : @code
end

def to_formatted_str
"#{bank_code} #{country_code} #{location_code} #{branch_code}".strip
end

def test?
location_code[1] == '0'
end

def passive?
location_code[1] == '1'
end

def reverse_billing?
location_code[1] == '2'
end

def valid?
valid_format? && valid_location_code? && valid_branch_code?
end

def valid_format?
REGEX.match?(@code)
end

def valid_location_code?
!location_code.start_with?('0', '1') && !location_code.end_with?('O')
end

def valid_branch_code?
branch_code.empty? || branch_code == 'XXX' || !branch_code.start_with?('X')
end
end
end
46 changes: 23 additions & 23 deletions lib/girocode/code.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
module Girocode
class Error < StandardError; end

class Code
ATTRIBUTES = %i[bic name iban currency amount purpose creditor_reference reference bto_info]
attr_reader *ATTRIBUTES

MAX_PAYLOAD_BYTES = 331
AMOUNT_RANGE = BigDecimal('0.01')..BigDecimal('999999999.99')

def initialize(**attrs)
if keys = attrs.keys - ATTRIBUTES and not keys.empty?
raise ArgumentError, "Illegal attributes #{keys.inspect}"
Expand All @@ -19,99 +19,99 @@ def initialize(**attrs)
raise ArgumentError, "either creditor reference or reference may be set" if creditor_reference? && reference?
raise ArgumentError, "payload too long" if payload.bytesize > MAX_PAYLOAD_BYTES
end

def bic=(value)
if value.nil?
@bic = nil
else
bic = Bank::BIC.new(value)
bic = BIC.new(value)
raise ArgumentError, "Invalid BIC #{value.inspect}" unless bic.valid?
@bic = bic.to_s
end
end

def name=(value)
value = value.strip
raise ArgumentError, 'name is required' unless value
raise ArgumentError, 'name too long' if value.size > 70
raise ArgumentError, 'Illegal name' if value.include?("\n") || value.include?("\r")
@name = value
end

def iban=(value)
iban = Bank::IBAN.new(value)
raise ArgumentError, "Invalid IBAN #{value.inspect}" unless iban.valid?
@iban = iban.to_s
raise ArgumentError, "Invalid IBAN #{value.inspect}" unless IBANTools::IBAN.valid?(value)

@iban = value
end

def currency=(value)
value = value.to_s.upcase
raise ArgumentError, "Invalid currency" unless value.match?(/\A[A-Z]{3}\z/)
@currency = value
end

def amount=(value)
raise ArgumentError, 'amount is required' unless value
value = BigDecimal(value, Float::DIG + 1)
raise ArgumentError, "invalid amount #{value.inspect}" unless AMOUNT_RANGE.cover?(value)
@amount = value
end

def purpose=(value)
unless value.nil?
raise ArgumentError, "invalid purpose #{value.inspect}" unless value.match?(/\A[A-z0-9]{0,4}\z/)
end
@purpose = value
end

def creditor_reference=(value)
unless value.nil?
raise ArgumentError, "invalid creditor reference #{value.inspect}" if value.include?("\n") || value.include?("\r") || value.size > 35
end
@creditor_reference = value
end

def reference=(value)
unless value.nil?
raise ArgumentError, "invalid reference #{value.inspect}" if value.include?("\n") || value.include?("\r") || value.size > 140
end
@reference = value
end

def bto_info=(value)
unless value.nil?
raise ArgumentError, "invalid bto_info #{value.inspect}" if value.include?("\n") || value.include?("\r") || value.size > 70
end
@bto_info = value
end

ATTRIBUTES.each do |attr|
define_method("#{attr}?") do
value = instance_variable_get("@#{attr}")
value.respond_to?(:empty?) ? !value.empty? : !!value
end
end

def payload
['BCD', '002', '1', 'SCT',
bic, name, iban,formatted_amount, purpose,
creditor_reference || reference, bto_info].map(&:to_s).join("\n")
end

def to_qrcode
RQRCode::QRCode.new(payload, level: :m, mode: :byte_8bit)
end

def to_ascii
to_qrcode.to_s
end

%i[png svg html ansi].each do |format|
define_method("to_#{format}") { |*args| to_qrcode.public_send("as_#{format}", *args) }
end

private

def formatted_amount
"#{currency}#{amount.round(2).to_s('F')}" if currency? && amount
end
Expand Down
2 changes: 1 addition & 1 deletion lib/girocode/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module Girocode
VERSION = "0.2.0"
VERSION = "1.0.0"
end
12 changes: 9 additions & 3 deletions test/girocode_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,23 @@

class GirocodeTest < Minitest::Test
def test_that_it_has_a_version_number
refute_nil ::Girocode::VERSION
refute_nil Girocode::VERSION
end

def test_girocode
attrs = { bic: 'BHBLDEHHXXX', name: 'Franz Mustermänn', iban: 'DE71110220330123456789', currency: :eur, amount: 12.3, purpose: 'GDDS', creditor_reference: 'RF18539007547034' }
code = Girocode.new(**attrs)
assert_equal data(:data), code.to_ascii
end


def test_bic
assert_raises ArgumentError do
Girocode.new(bic: 'FOOBAR', name: 'Franz Mustermänn', iban: 'DE71110220330123456789', currency: :eur, amount: 12.3, purpose: 'GDDS', creditor_reference: 'RF18539007547034')
end
end

private

def data(name)
Pathname(__dir__).join("#{name}.txt").read
end
Expand Down