├── .ruby-version ├── .rspec ├── Gemfile ├── .gitignore ├── imgs ├── qr_bill_empty.png ├── qr_bill_no_amount.jpeg ├── qr_bill_red_no_ref.jpeg ├── qr_bill_orange_old_ref.jpeg ├── qr_bill_with_old_reference.png ├── qr_bill_without_reference.png ├── qr_bill_red_with_credit_ref.jpeg └── qr_bill_with_creditor_reference.png ├── web └── assets │ ├── images │ ├── swiss_cross.png │ ├── amoint_40x15mm.png │ ├── scissors_symbol.png │ ├── payable_by_65x25mm.png │ └── swiss_cross.svg │ └── fonts │ ├── LiberationSans-Regular.eot │ ├── LiberationSans-Regular.ttf │ ├── LiberationSans-Regular.woff │ └── LiberationSans-Regular.svg ├── lib ├── qr-bills │ ├── qr-exceptions.rb │ ├── qr-creditor-reference.rb │ ├── qr-params.rb │ ├── qr-generator.rb │ └── qr-html-layout.rb └── qr-bills.rb ├── Rakefile ├── config └── locales │ ├── qrbills.de.yml │ ├── qrbills.en.yml │ ├── qrbills.fr.yml │ └── qrbills.it.yml ├── .circleci └── config.yml ├── Gemfile.lock ├── spec ├── qr-creditor-reference_spec.rb ├── qr-bills_spec.rb ├── qr-generator_spec.rb ├── qr-html-layout_spec.rb ├── spec_helper.rb ├── qr-params_spec.rb └── fixtures │ └── qrcode.svg ├── qr-bills.gemspec ├── LICENSE ├── CHANGELOG.md ├── qr-layout.html └── README.md /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.2.2 -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.idea 2 | *.gem 3 | tmp/* 4 | .byebug 5 | .byebug_history 6 | -------------------------------------------------------------------------------- /imgs/qr_bill_empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damoiser/qr-bills/HEAD/imgs/qr_bill_empty.png -------------------------------------------------------------------------------- /imgs/qr_bill_no_amount.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damoiser/qr-bills/HEAD/imgs/qr_bill_no_amount.jpeg -------------------------------------------------------------------------------- /imgs/qr_bill_red_no_ref.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damoiser/qr-bills/HEAD/imgs/qr_bill_red_no_ref.jpeg -------------------------------------------------------------------------------- /imgs/qr_bill_orange_old_ref.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damoiser/qr-bills/HEAD/imgs/qr_bill_orange_old_ref.jpeg -------------------------------------------------------------------------------- /imgs/qr_bill_with_old_reference.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damoiser/qr-bills/HEAD/imgs/qr_bill_with_old_reference.png -------------------------------------------------------------------------------- /imgs/qr_bill_without_reference.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damoiser/qr-bills/HEAD/imgs/qr_bill_without_reference.png -------------------------------------------------------------------------------- /web/assets/images/swiss_cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damoiser/qr-bills/HEAD/web/assets/images/swiss_cross.png -------------------------------------------------------------------------------- /imgs/qr_bill_red_with_credit_ref.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damoiser/qr-bills/HEAD/imgs/qr_bill_red_with_credit_ref.jpeg -------------------------------------------------------------------------------- /web/assets/images/amoint_40x15mm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damoiser/qr-bills/HEAD/web/assets/images/amoint_40x15mm.png -------------------------------------------------------------------------------- /web/assets/images/scissors_symbol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damoiser/qr-bills/HEAD/web/assets/images/scissors_symbol.png -------------------------------------------------------------------------------- /imgs/qr_bill_with_creditor_reference.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damoiser/qr-bills/HEAD/imgs/qr_bill_with_creditor_reference.png -------------------------------------------------------------------------------- /web/assets/images/payable_by_65x25mm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damoiser/qr-bills/HEAD/web/assets/images/payable_by_65x25mm.png -------------------------------------------------------------------------------- /web/assets/fonts/LiberationSans-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damoiser/qr-bills/HEAD/web/assets/fonts/LiberationSans-Regular.eot -------------------------------------------------------------------------------- /web/assets/fonts/LiberationSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damoiser/qr-bills/HEAD/web/assets/fonts/LiberationSans-Regular.ttf -------------------------------------------------------------------------------- /web/assets/fonts/LiberationSans-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/damoiser/qr-bills/HEAD/web/assets/fonts/LiberationSans-Regular.woff -------------------------------------------------------------------------------- /lib/qr-bills/qr-exceptions.rb: -------------------------------------------------------------------------------- 1 | module QRExceptions 2 | EXCEPTION_PREFIX = "QR-bill" 3 | 4 | INVALID_PARAMETERS = EXCEPTION_PREFIX + " invalid parameters" 5 | NOT_SUPPORTED = EXCEPTION_PREFIX + " not supported" 6 | end -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rspec/core/rake_task' 2 | require 'bundler/gem_tasks'# Default directory to look in is `/spec` 3 | 4 | RSpec::Core::RakeTask.new(:spec) do |t| 5 | t.pattern = Dir.glob("spec/**/*_spec.rb") 6 | t.rspec_opts = "--format documentation" 7 | end 8 | 9 | task :default => :spec -------------------------------------------------------------------------------- /config/locales/qrbills.de.yml: -------------------------------------------------------------------------------- 1 | de: 2 | qrbills: 3 | payment_part: Zahlteil 4 | account: Konto 5 | payable_to: Zahlbar an 6 | reference: Referenz 7 | additional_information: zusätzliche Informationen 8 | further_information: weitere Informationen 9 | currency: Währung 10 | amount: Betrag 11 | receipt: Empfangsschein 12 | acceptance_point: Annahmestelle 13 | separate_before_paying_in: Vor der Einzahlung abzutrennen 14 | payable_by: Zahlbar durch 15 | payable_by_name_addr: Zahlbar durch (Name/Adresse) 16 | in_favour_of: Zugunsten 17 | name: Name -------------------------------------------------------------------------------- /config/locales/qrbills.en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | qrbills: 3 | payment_part: payment part 4 | account: account 5 | payable_to: payable to 6 | reference: reference 7 | additional_information: additional information 8 | further_information: further information 9 | currency: currency 10 | amount: amount 11 | receipt: receipt 12 | acceptance_point: acceptance point 13 | separate_before_paying_in: separate before paying in 14 | payable_by: payable by 15 | payable_by_name_addr: payable by (name/address) 16 | in_favour_of: in favour of 17 | name: name 18 | -------------------------------------------------------------------------------- /config/locales/qrbills.fr.yml: -------------------------------------------------------------------------------- 1 | fr: 2 | qrbills: 3 | payment_part: section paiement 4 | account: compte 5 | payable_to: payable à 6 | reference: référence 7 | additional_information: informations additionnelles 8 | further_information: informations supplémentaires 9 | currency: monnaie 10 | amount: montant 11 | receipt: récépissé 12 | acceptance_point: point de dépôt 13 | separate_before_paying_in: à détacher avant le versement 14 | payable_by: payable par 15 | payable_by_name_addr: payable par (nom/adresse) 16 | in_favour_of: en faveur de 17 | name: nom 18 | -------------------------------------------------------------------------------- /config/locales/qrbills.it.yml: -------------------------------------------------------------------------------- 1 | it: 2 | qrbills: 3 | payment_part: "sezione pagamento" 4 | account: conto 5 | payable_to: "pagabile a" 6 | reference: riferimento 7 | additional_information: "informazioni aggiuntive" 8 | further_information: "informazioni supplementari" 9 | currency: valuta 10 | amount: importo 11 | receipt: ricevuta 12 | acceptance_point: "punto di accettazione" 13 | separate_before_paying_in: "da staccare prima del versamento" 14 | payable_by: pagabile da 15 | payable_by_name_addr: pagabile da (nome/indirizzo) 16 | in_favour_of: a favore di 17 | name: nome -------------------------------------------------------------------------------- /web/assets/images/swiss_cross.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Use the latest 2.1 version of CircleCI pipeline process engine. 2 | # See: https://circleci.com/docs/2.0/configuration-reference 3 | version: 2.1 4 | 5 | # Orbs are reusable packages of CircleCI configuration that you may share across projects, enabling you to create encapsulated, parameterized commands, jobs, and executors that can be used across multiple projects. 6 | # See: https://circleci.com/docs/2.0/orb-intro/ 7 | orbs: 8 | ruby: circleci/ruby@0.1.2 9 | 10 | # Define a job to be invoked later in a workflow. 11 | # See: https://circleci.com/docs/2.0/configuration-reference/#jobs 12 | jobs: 13 | test: 14 | docker: 15 | - image: cimg/ruby:3.2.2 16 | executor: ruby/default 17 | resource_class: medium 18 | steps: 19 | - checkout 20 | - run: bundle install 21 | - run: bundle list 22 | - run: bundle exec rake spec 23 | 24 | # Invoke jobs via workflows 25 | # See: https://circleci.com/docs/2.0/configuration-reference/#workflows 26 | workflows: 27 | test-pipeline: 28 | jobs: 29 | - test 30 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | qr-bills (1.0.10) 5 | i18n (>= 1.8.3, < 2) 6 | rqrcode (>= 2.1, < 3) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | byebug (11.1.3) 12 | chunky_png (1.4.0) 13 | coderay (1.1.3) 14 | concurrent-ruby (1.3.5) 15 | diff-lcs (1.5.0) 16 | i18n (1.14.7) 17 | concurrent-ruby (~> 1.0) 18 | method_source (1.0.0) 19 | pry (0.14.1) 20 | coderay (~> 1.1) 21 | method_source (~> 1.0) 22 | rake (13.0.6) 23 | rqrcode (2.2.0) 24 | chunky_png (~> 1.0) 25 | rqrcode_core (~> 1.0) 26 | rqrcode_core (1.2.0) 27 | rspec (3.11.0) 28 | rspec-core (~> 3.11.0) 29 | rspec-expectations (~> 3.11.0) 30 | rspec-mocks (~> 3.11.0) 31 | rspec-core (3.11.0) 32 | rspec-support (~> 3.11.0) 33 | rspec-expectations (3.11.0) 34 | diff-lcs (>= 1.2.0, < 2.0) 35 | rspec-support (~> 3.11.0) 36 | rspec-mocks (3.11.1) 37 | diff-lcs (>= 1.2.0, < 2.0) 38 | rspec-support (~> 3.11.0) 39 | rspec-support (3.11.0) 40 | 41 | PLATFORMS 42 | ruby 43 | 44 | DEPENDENCIES 45 | byebug 46 | pry 47 | qr-bills! 48 | rake (~> 13.0) 49 | rspec (~> 3.9) 50 | 51 | BUNDLED WITH 52 | 2.1.4 53 | -------------------------------------------------------------------------------- /spec/qr-creditor-reference_spec.rb: -------------------------------------------------------------------------------- 1 | require 'qr-bills' 2 | require 'qr-bills/qr-creditor-reference' 3 | 4 | RSpec.describe "QRCreditorReference" do 5 | describe "checking the helpers" do 6 | it "works as expected" do 7 | expect(QRBills.create_creditor_reference("MTR81UUWZYO48NY55NP3")).to eq("RF89MTR81UUWZYO48NY55NP3") 8 | end 9 | end 10 | 11 | describe "validate the input correctly" do 12 | it "checks that reference should be less than 21 chars" do 13 | expect{QRCreditorReference.create("ABCDABCDABCDABCDABCDABCD")}.to raise_error 14 | end 15 | 16 | it "checks that reference should be greater or equal than 1 char" do 17 | expect{QRCreditorReference.create("")}.to raise_error 18 | end 19 | end 20 | 21 | describe "produce the correct reference" do 22 | it "produces the right one [1]" do 23 | expect(QRCreditorReference.create("MTR81UUWZYO48NY55NP3")).to eq("RF89MTR81UUWZYO48NY55NP3") 24 | end 25 | 26 | it "produces the right one [2]" do 27 | expect(QRCreditorReference.create("539007547034")).to eq("RF18539007547034") 28 | end 29 | 30 | it "produces the right one [3]" do 31 | expect(QRCreditorReference.create("539 007 5470 34 ")).to eq("RF18539007547034") 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/qr-bills_spec.rb: -------------------------------------------------------------------------------- 1 | require 'qr-bills' 2 | 3 | RSpec.describe QRBills do 4 | describe "init" do 5 | it "raise an exception if the bill kind is not set" do 6 | expect { QRBills.generate({}) }.to raise_error(ArgumentError, "QR-bill invalid parameters: bill type param not set") 7 | end 8 | 9 | it "raise an exception if currency is not set" do 10 | expect { QRBills.generate(bill_type: "bad") }.to raise_error(ArgumentError, "QR-bill invalid parameters: currency cannot be blank") 11 | end 12 | 13 | it "raise an exception if bill type is not supported" do 14 | expect { QRBills.generate(bill_type: "bad", bill_params: { currency: 'CHF' }) }.to raise_error(ArgumentError, "QR-bill invalid parameters: bill type is not supported") 15 | end 16 | 17 | it "raise an exception if params for ESR is less than 26 chars" do 18 | expect { QRBills.create_esr_creditor_reference("123") }.to raise_error(ArgumentError, "QR-bill invalid parameters: You must provide a 26 digit reference for ESR.") 19 | end 20 | 21 | it "raise an exception if params for ESR is not an integer" do 22 | expect { QRBills.create_esr_creditor_reference("aabbccddeeffgghhiijjkkllmm") }.to raise_error(ArgumentError, "QR-bill invalid parameters: You must provide a valid digit for ESR.") 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /qr-bills.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = "qr-bills" 3 | s.version = "1.0.11" 4 | s.date = "2025-09-30" 5 | s.summary = "QR-bills support for swiss payments" 6 | s.description = "QR-bills support for swiss payments, for full documentation please refer to github repo: https://github.com/damoiser/qr-bills" 7 | s.authors = ["Damiano Radice"] 8 | s.email = "dam.radice@gmail.com" 9 | s.files = Dir["lib/**/*"] + Dir["web/assets/**/*"] + Dir["config/locales/*.yml"] 10 | s.require_paths = ["lib"] 11 | s.homepage = "https://github.com/damoiser/qr-bills" 12 | s.license = "BSD-3-Clause" 13 | s.metadata = { 14 | "bug_tracker_uri" => "https://github.com/damoiser/qr-bills/issues", 15 | "changelog_uri" => "https://github.com/damoiser/qr-bills/CHANGELOG.md", 16 | "documentation_uri" => "https://github.com/damoiser/qr-bills/README.md", 17 | "homepage_uri" => "https://github.com/damoiser/qr-bills", 18 | "source_code_uri" => "https://github.com/damoiser/qr-bills", 19 | "wiki_uri" => "https://github.com/damoiser/qr-bills" 20 | } 21 | s.required_ruby_version = ">= 3.2.2" 22 | s.add_runtime_dependency("i18n", ">= 1.8.3", "< 2") 23 | s.add_runtime_dependency("rqrcode", ">= 2.1", "< 3") 24 | s.add_development_dependency("rspec", "~> 3.9") 25 | s.add_development_dependency("rake", "~> 13.0") 26 | s.add_development_dependency("pry") 27 | s.add_development_dependency("byebug") 28 | end 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, Damiano Radice (damoiser), dam.radice@gmail.com 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### v1.0.11 2 | * bump ruby version to 3.2.2 3 | * add support for ESR reference 4 | 5 | ### v1.0.10 6 | * fixed pipeline error (new svg style attribute) -> style=fill:#000 7 | * fixed locales error for EN and FR 8 | 9 | ### v1.0.9 10 | * added support for address of type "K" 11 | 12 | ### v1.0.8 13 | * added SVG support 14 | 15 | ### v1.0.7 16 | * make default locale dynamic 17 | 18 | ### v1.0.6 19 | * fix amount formatting ('%.2f') - in generator as well 20 | * adjust qr error code to m as specs: see PR#18 - in generator as well 21 | 22 | ### v1.0.5 23 | * fix amount formatting ('%.2f') 24 | * adjust qr error code to m as specs: see PR#18 25 | 26 | ### v1.0.4 27 | * remove rake dependency 28 | 29 | ### v1.0.3 30 | * switch CI from Travis to CircleCI 31 | 32 | ### v1.0.2 33 | * fix typo for French 34 | * fix locale setting in html template 35 | 36 | ### v1.0.1 37 | * fix typo in constant 38 | 39 | ### v1 40 | * change license to BSD-3 41 | * improve tests 42 | * improve comments 43 | * improve validations for supported qr bills 44 | * improve readme / documentation 45 | * fix typo in param: alternative_scheme_parameters 46 | 47 | ### v0.3 48 | * refactoring and fix typos 49 | * remove RMagick and use ChunkyPNG 50 | * add output format qrcode-png 51 | * fix import of locales 52 | 53 | ### v0.2.2 54 | * fix locales / translations 55 | 56 | ### v0.2.1 57 | * add locales path params to reference from Rails 58 | * remove deprecation warning of RMagick dependency 59 | 60 | ### v0.2 61 | * add creditor reference ISO-11649 generator 62 | 63 | ### v0.1.2 64 | * fix layout 65 | 66 | ### v0.1.1 67 | * fix generator 68 | 69 | ### v0.1 70 | * first release 71 | * qr generation 72 | * multilanguage support 73 | * export as html-layout -------------------------------------------------------------------------------- /lib/qr-bills/qr-creditor-reference.rb: -------------------------------------------------------------------------------- 1 | require 'qr-bills/qr-exceptions' 2 | 3 | # implement Creditor Reference ISO 11649 generator 4 | module QRCreditorReference 5 | PREFIX = "RF" 6 | 7 | # chars values to calculate the check code 8 | @char_values = { 9 | "A": 10, 10 | "B": 11, 11 | "C": 12, 12 | "D": 13, 13 | "E": 14, 14 | "F": 15, 15 | "G": 16, 16 | "H": 17, 17 | "I": 18, 18 | "J": 19, 19 | "K": 20, 20 | "L": 21, 21 | "M": 22, 22 | "N": 23, 23 | "O": 24, 24 | "P": 25, 25 | "Q": 26, 26 | "R": 27, 27 | "S": 28, 28 | "T": 29, 29 | "U": 30, 30 | "V": 31, 31 | "W": 32, 32 | "X": 33, 33 | "Y": 34, 34 | "Z": 35 35 | } 36 | 37 | def self.create(reference) 38 | reference = reference.delete(' ') 39 | chars = reference.split('') 40 | 41 | if chars.size == 0 42 | raise QRExceptions::INVALID_PARAMETERS + ": provided reference too short: must be at least one char" 43 | end 44 | 45 | # max 25 chars: 2 chars (RF) + 2 chars (check code) + 21 chars (reference) 46 | if chars.size > 21 47 | raise QRExceptions::INVALID_PARAMETERS + ": provided reference too long: must be less than 21 chars" 48 | end 49 | 50 | reference_val = "" 51 | 52 | chars.each do |c| 53 | reference_val += get_char_value(c).to_s 54 | end 55 | 56 | # put RF+00 at the end to resolve check code 57 | reference_val += @char_values["R".to_sym].to_s + @char_values["F".to_sym].to_s + "00" 58 | 59 | # get check code 60 | # when performing the validation of the check code (n % 97) 61 | # the remainder must be equal to 1, thus the 98 62 | check_code = 98 - (reference_val.to_i % 97) 63 | 64 | if check_code < 10 65 | check_code = "0" + check_code.to_s 66 | end 67 | 68 | return PREFIX + check_code.to_s + reference 69 | end 70 | 71 | def self.get_char_value(char) 72 | if char =~ /[0-9]/ 73 | return char.to_i 74 | end 75 | 76 | return @char_values[char.upcase.to_sym] 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /spec/qr-generator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'qr-bills' 2 | require 'fileutils' 3 | require 'qr-bills/qr-generator' 4 | 5 | RSpec.describe QRGenerator do 6 | before do 7 | FileUtils.mkdir_p "#{Dir.pwd}/tmp/" 8 | File.delete filepath if File.exist?(filepath) 9 | end 10 | 11 | let(:filepath) { "#{Dir.pwd}/tmp/qrcode.png" } 12 | let(:params) do 13 | QRParams.get_qr_params.tap do |params_hash| 14 | params_hash[:bill_params][:creditor][:iban] = "CH93 0076 2011 6238 5295 7" 15 | params_hash[:bill_params][:creditor][:address][:type] = "S" 16 | params_hash[:bill_params][:creditor][:address][:name] = "Compagnia di assicurazione forma & scalciante" 17 | params_hash[:bill_params][:creditor][:address][:line1] = "Via cantonale" 18 | params_hash[:bill_params][:creditor][:address][:line2] = "24" 19 | params_hash[:bill_params][:creditor][:address][:postal_code] = "3000" 20 | params_hash[:bill_params][:creditor][:address][:town] = "Lugano" 21 | params_hash[:bill_params][:creditor][:address][:country] = "CH" 22 | params_hash[:bill_params][:amount] = 12345.15 23 | params_hash[:bill_params][:currency] = "CHF" 24 | params_hash[:bill_params][:debtor][:address][:type] = "S" 25 | params_hash[:bill_params][:debtor][:address][:name] = "Foobar Barfoot" 26 | params_hash[:bill_params][:debtor][:address][:line1] = "Via cantonale" 27 | params_hash[:bill_params][:debtor][:address][:line2] = "25" 28 | params_hash[:bill_params][:debtor][:address][:postal_code] = "3001" 29 | params_hash[:bill_params][:debtor][:address][:town] = "Comano" 30 | params_hash[:bill_params][:debtor][:address][:country] = "CH" 31 | params_hash[:bill_params][:reference] = "RF89MTR81UUWZYO48NY55NP3" 32 | params_hash[:bill_params][:reference_type] = "SCOR" 33 | params_hash[:bill_params][:additionally_information] = "pagamento riparazione monopattino" 34 | end 35 | end 36 | 37 | describe "qrcode generation" do 38 | it "generates successfully a qr image" do 39 | params[:qrcode_format] = 'qrcode_png' 40 | 41 | expect(File.exist?(filepath)).to be_falsy 42 | expect{QRGenerator.create(params, filepath)}.not_to raise_error 43 | expect(File.exist?(filepath)).to be_truthy 44 | end 45 | 46 | it "generates a png image" do 47 | params[:qrcode_format] = 'png' 48 | 49 | png = QRGenerator.create(params) 50 | expect(png.class).to be(ChunkyPNG::Image) 51 | end 52 | 53 | it "generates a svg string" do 54 | params[:qrcode_format] = 'svg' 55 | 56 | svg = QRGenerator.create(params) 57 | File.write('tmp/qrcode.svg', svg) 58 | file = File.open('spec/fixtures/qrcode.svg').read 59 | expect(svg).to eq(file) 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/qr-bills.rb: -------------------------------------------------------------------------------- 1 | require 'i18n' 2 | require 'qr-bills/qr-exceptions' 3 | require 'qr-bills/qr-params' 4 | require 'qr-bills/qr-html-layout' 5 | require 'qr-bills/qr-creditor-reference' 6 | 7 | module QRBills 8 | def self.generate(qr_params) 9 | raise ArgumentError, "#{QRExceptions::INVALID_PARAMETERS}: bill type param not set" unless qr_params.has_key?(:bill_type) 10 | raise ArgumentError, "#{QRExceptions::INVALID_PARAMETERS}: validation failed" unless QRParams.valid?(qr_params) 11 | 12 | # init translator sets 13 | %i[it en de fr].each do |locale| 14 | locale_file = File.join(qr_params[:locales][:path], "qrbills.#{locale}.yml") 15 | 16 | I18n.load_path << locale_file 17 | end 18 | 19 | output = case qr_params[:output_params][:format] 20 | when 'html' 21 | QRHTMLLayout.create(qr_params) 22 | else 23 | QRGenerator.create(qr_params, qr_params[:qrcode_filepath]) 24 | end 25 | 26 | { params: qr_params, output: output } 27 | end 28 | 29 | # Given a creditor's IBAN number, this method checks whether an IBAN is of the new qr or the legacy esr type. 30 | # When generating a bill with a reference number, that number must be generated using the following method if this helper returns: 31 | # - :qr => create_creditor_reference 32 | # - :esr => create_esr_creditor_reference 33 | def self.iban_type(iban) 34 | return nil if iban.blank? 35 | iban_institute_identifier = iban.strip.gsub(' ', '')[4..8].to_i 36 | return iban_institute_identifier.between?(30_000, 31_999) ? :qr : :esr 37 | end 38 | 39 | def self.create_creditor_reference(reference) 40 | QRCreditorReference.create(reference) 41 | end 42 | 43 | # ESR reference should be considered "deprecated" and is here for backward compatibility 44 | # This is based on: http://sahits.ch/blog/blog/2007/11/08/uberprufen-esr-referenz-nummer/ 45 | def self.create_esr_creditor_reference(reference) 46 | raise ArgumentError, "#{QRExceptions::INVALID_PARAMETERS}: You must provide a 26 digit reference for ESR." unless reference.size == 26 47 | raise ArgumentError, "#{QRExceptions::INVALID_PARAMETERS}: You must provide a valid digit for ESR." unless reference.to_i.to_s == reference 48 | 49 | esr = "#{reference}0" 50 | lookup_table = [0, 9, 4, 6, 8, 2, 7, 1, 3, 5] 51 | next_val = 0 52 | 53 | (0...esr.length - 1).each do |i| 54 | ch = esr[i] 55 | n = ch.to_i 56 | index = (next_val + n) % 10 57 | next_val = lookup_table[index] 58 | end 59 | 60 | result = (10 - next_val) % 10 61 | return result 62 | end 63 | 64 | def self.get_qr_params 65 | QRParams.get_qr_params 66 | end 67 | 68 | def self.get_qrbill_with_qr_reference_type 69 | QRParams::QR_BILL_WITH_QR_REFERENCE 70 | end 71 | 72 | def self.get_qrbill_with_creditor_reference_type 73 | QRParams::QR_BILL_WITH_CREDITOR_REFERENCE 74 | end 75 | 76 | def self.get_qrbill_without_reference_type 77 | QRParams::QR_BILL_WITHOUT_REFERENCE 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/qr-bills/qr-params.rb: -------------------------------------------------------------------------------- 1 | module QRParams 2 | QR_BILL_WITH_QR_REFERENCE = "orange_with_reference" 3 | QR_BILL_WITH_CREDITOR_REFERENCE = "red_with_reference" 4 | QR_BILL_WITHOUT_REFERENCE = "red_without_reference" 5 | 6 | def self.get_qr_params 7 | { 8 | bill_type: "", # see global variables / README 9 | qrcode_format: nil, # png or svg, overwrites qrcode_filepath 10 | qrcode_filepath: "", # deprecated, where to store the qrcode, i.e. : /tmp/qrcode_1234.png 11 | fonts: { 12 | eot: File.expand_path("#{File.dirname(__FILE__)}/../../web/assets/fonts/LiberationSans-Regular.eot"), 13 | woff: File.expand_path("#{File.dirname(__FILE__)}/../../web/assets/fonts/LiberationSans-Regular.woff"), 14 | ttf: File.expand_path("#{File.dirname(__FILE__)}/../../web/assets/fonts/LiberationSans-Regular.ttf"), 15 | svg: File.expand_path("#{File.dirname(__FILE__)}/../../web/assets/fonts/LiberationSans-Regular.svg") 16 | }, 17 | locales: { 18 | path: File.expand_path("#{File.dirname(__FILE__)}/../../config/locales") 19 | }, 20 | bill_params: { 21 | language: I18n.locale, 22 | amount: 0.0, 23 | currency: "CHF", 24 | reference_type: "", # QRR = QR reference, SCOR = Creditor reference, NON = without reference 25 | reference: "", # qr reference or creditor reference (iso-11649) 26 | additionally_information: "", 27 | bill_information_coded: "", 28 | alternative_scheme_parameters: "", 29 | creditor: { 30 | address: { 31 | type: "S", 32 | name: "", 33 | line1: "", 34 | line2: "", 35 | postal_code: "", 36 | town: "", 37 | country: "", 38 | iban: "" 39 | }, 40 | }, 41 | debtor: { 42 | address: { 43 | type: "S", 44 | name: "", 45 | line1: "", 46 | line2: "", 47 | postal_code: "", 48 | town: "", 49 | country: "", 50 | }, 51 | } 52 | }, 53 | output_params: { 54 | format: "html" 55 | } 56 | } 57 | end 58 | 59 | def self.valid?(params) 60 | return false unless params.key?(:bill_type) 61 | return false unless QRParams.base_params_valid?(params) 62 | 63 | case params[:bill_type] 64 | when QRParams::QR_BILL_WITH_QR_REFERENCE 65 | QRParams.qr_bill_with_qr_reference_valid?(params) 66 | when QRParams::QR_BILL_WITH_CREDITOR_REFERENCE 67 | QRParams.qr_bill_with_creditor_reference_valid?(params) 68 | when QRParams::QR_BILL_WITHOUT_REFERENCE 69 | QRParams.qr_bill_without_reference_valid?(params) 70 | else 71 | raise ArgumentError, "#{QRExceptions::INVALID_PARAMETERS}: bill type is not supported" 72 | end 73 | end 74 | 75 | def self.base_params_valid?(params) 76 | if params[:bill_type] == "" || params[:bill_type] == nil 77 | raise ArgumentError, "#{QRExceptions::INVALID_PARAMETERS}: bill type cannot be blank" 78 | end 79 | 80 | if params.dig(:bill_params, :currency) == "" || params.dig(:bill_params, :currency) == nil 81 | raise ArgumentError, "#{QRExceptions::INVALID_PARAMETERS}: currency cannot be blank" 82 | end 83 | 84 | true 85 | end 86 | 87 | def self.qr_bill_with_qr_reference_valid?(params) 88 | if params[:bill_params][:reference_type] != "QRR" 89 | raise ArgumentError, "#{QRExceptions::INVALID_PARAMETERS}: reference type must be 'QRR' for QR bill with standard reference" 90 | end 91 | 92 | if params[:bill_params][:reference] == "" || params[:bill_params][:reference] == nil 93 | raise ArgumentError, "#{QRExceptions::INVALID_PARAMETERS}: reference cannot be blank for QR bill with standard reference" 94 | end 95 | 96 | true 97 | end 98 | 99 | def self.qr_bill_with_creditor_reference_valid?(params) 100 | if params[:bill_params][:reference_type] != "SCOR" 101 | raise ArgumentError, "#{QRExceptions::INVALID_PARAMETERS}: reference type must be 'SCOR' for QR bill with (new) creditor reference" 102 | end 103 | 104 | if params[:bill_params][:reference] == "" || params[:bill_params][:reference] == nil 105 | raise ArgumentError, "#{QRExceptions::INVALID_PARAMETERS}: reference cannot be blank for QR bill with (new) creditor reference" 106 | end 107 | 108 | true 109 | end 110 | 111 | def self.qr_bill_without_reference_valid?(params) 112 | if params[:bill_params][:reference_type] != "NON" 113 | raise ArgumentError, "#{QRExceptions::INVALID_PARAMETERS}: reference type must be 'NON' for QR bill without reference" 114 | end 115 | 116 | if params[:bill_params][:reference] != "" 117 | raise ArgumentError, "#{QRExceptions::INVALID_PARAMETERS}: reference must be blank for QR bill without reference" 118 | end 119 | 120 | true 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /spec/qr-html-layout_spec.rb: -------------------------------------------------------------------------------- 1 | require 'i18n' 2 | require 'fileutils' 3 | require 'qr-bills/qr-html-layout' 4 | require 'qr-bills/qr-params' 5 | 6 | RSpec.configure do |config| 7 | config.before(:each) do 8 | @params = QRParams.get_qr_params 9 | @params[:fonts][:eot] = "../web/assets/fonts/LiberationSans-Regular.eot" 10 | @params[:fonts][:woff] = "../web/assets/fonts/LiberationSans-Regular.woff" 11 | @params[:fonts][:ttf] = "../web/assets/fonts/LiberationSans-Regular.ttf" 12 | @params[:fonts][:svg] = "../web/assets/fonts/LiberationSans-Regular.svg" 13 | @params[:locales][:path] = "config/locales/" 14 | @params[:qrcode_format] = 'png' 15 | @params[:bill_params][:creditor][:iban] = "CH9300762011623852957" 16 | @params[:bill_params][:creditor][:address][:type] = "S" 17 | @params[:bill_params][:creditor][:address][:name] = "Compagnia di assicurazione forma & scalciante" 18 | @params[:bill_params][:creditor][:address][:line1] = "Via cantonale" 19 | @params[:bill_params][:creditor][:address][:line2] = "24" 20 | @params[:bill_params][:creditor][:address][:postal_code] = "3000" 21 | @params[:bill_params][:creditor][:address][:town] = "Lugano" 22 | @params[:bill_params][:creditor][:address][:country] = "CH" 23 | @params[:bill_params][:amount] = 12345.15 24 | @params[:bill_params][:currency] = "CHF" 25 | @params[:bill_params][:debtor][:address][:type] = "S" 26 | @params[:bill_params][:debtor][:address][:name] = "Foobar Barfoot" 27 | @params[:bill_params][:debtor][:address][:line1] = "Via cantonale" 28 | @params[:bill_params][:debtor][:address][:line2] = "25" 29 | @params[:bill_params][:debtor][:address][:postal_code] = "3001" 30 | @params[:bill_params][:debtor][:address][:town] = "Comano" 31 | @params[:bill_params][:debtor][:address][:country] = "CH" 32 | @params[:bill_params][:reference] = "RF89MTR81UUWZYO48NY55NP3" 33 | @params[:bill_params][:reference_type] = "SCOR" 34 | @params[:bill_params][:additionally_information] = "pagamento riparazione monopattino" 35 | 36 | I18n.load_path << File.join(@params[:locales][:path], "qrbills.it.yml") 37 | I18n.load_path << File.join(@params[:locales][:path], "qrbills.en.yml") 38 | I18n.load_path << File.join(@params[:locales][:path], "qrbills.de.yml") 39 | I18n.load_path << File.join(@params[:locales][:path], "qrbills.fr.yml") 40 | I18n.default_locale = :it 41 | end 42 | end 43 | 44 | RSpec.describe "QRHTMLLayout" do 45 | before do 46 | FileUtils.mkdir_p "#{Dir.pwd}/tmp/" 47 | File.delete filepath if File.exist?(filepath) 48 | end 49 | 50 | let(:filepath) { "#{Dir.pwd}/tmp/html-layout.html" } 51 | 52 | describe "layout generation" do 53 | before do 54 | @params[:qrcode_format] = 'png' 55 | end 56 | 57 | it "generates successfully the html layout + qr code" do 58 | expect{QRHTMLLayout.create(@params)}.not_to raise_error 59 | end 60 | 61 | it "generates legacy png qrcode" do 62 | @params[:qrcode_format] = nil 63 | @params[:qrcode_filepath] = "#{Dir.pwd}/tmp/qrcode-html.png" 64 | 65 | IO.binwrite("#{Dir.pwd}/tmp/html-layout.html", QRHTMLLayout.create(@params).to_s) 66 | expect(File.exist?(filepath)).to be_truthy 67 | expect(File.exist?("#{Dir.pwd}/tmp/qrcode-html.png")).to be_truthy 68 | end 69 | 70 | it "generates png qrcode" do 71 | html_output = QRHTMLLayout.create(@params).to_s 72 | IO.binwrite(filepath, html_output) 73 | expect(File.exist?(filepath)).to be_truthy 74 | 75 | expect(html_output).to include("data:image/png;base64,") 76 | end 77 | 78 | it "generates svg qrcode" do 79 | @params[:qrcode_format] = 'svg' 80 | 81 | html_output = QRHTMLLayout.create(@params).to_s 82 | IO.binwrite(filepath, html_output) 83 | expect(File.exist?(filepath)).to be_truthy 84 | 85 | expect(html_output).to include("data:image/svg+xml;charset=utf-8,") 86 | end 87 | 88 | it "does not overwrite locale" do 89 | @params[:bill_params][:language] = :de 90 | 91 | QRHTMLLayout.create(@params) 92 | 93 | expect(I18n.locale).to be :it 94 | end 95 | 96 | it "rounds correctly (1)" do 97 | html_output = QRHTMLLayout.create(@params).to_s 98 | 99 | IO.binwrite(filepath, html_output) 100 | expect(File.exist?(filepath)).to be_truthy 101 | 102 | expect(html_output).to include("12345.15") 103 | end 104 | 105 | it "rounds correctly (2)" do 106 | @params[:bill_params][:amount] = 12345.1 107 | 108 | html_output = QRHTMLLayout.create(@params).to_s 109 | 110 | IO.binwrite(filepath, html_output) 111 | expect(File.exist?(filepath)).to be_truthy 112 | 113 | expect(html_output).to include("12345.10") 114 | end 115 | 116 | it "rounds correctly (3)" do 117 | @params[:bill_params][:amount] = 12345.10 118 | 119 | html_output = QRHTMLLayout.create(@params).to_s 120 | 121 | IO.binwrite(filepath, html_output) 122 | expect(File.exist?(filepath)).to be_truthy 123 | 124 | expect(html_output).to include("12345.10") 125 | end 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file was generated by the `rspec --init` command. Conventionally, all 2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 3 | # The generated `.rspec` file contains `--require spec_helper` which will cause 4 | # this file to always be loaded, without a need to explicitly require it in any 5 | # files. 6 | # 7 | # Given that it is always loaded, you are encouraged to keep this file as 8 | # light-weight as possible. Requiring heavyweight dependencies from this file 9 | # will add to the boot time of your test suite on EVERY test run, even for an 10 | # individual file that may not need all of that loaded. Instead, consider making 11 | # a separate helper file that requires the additional dependencies and performs 12 | # the additional setup, and require it from the spec files that actually need 13 | # it. 14 | # 15 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 16 | RSpec.configure do |config| 17 | # rspec-expectations config goes here. You can use an alternate 18 | # assertion/expectation library such as wrong or the stdlib/minitest 19 | # assertions if you prefer. 20 | config.expect_with :rspec do |expectations| 21 | # This option will default to `true` in RSpec 4. It makes the `description` 22 | # and `failure_message` of custom matchers include text for helper methods 23 | # defined using `chain`, e.g.: 24 | # be_bigger_than(2).and_smaller_than(4).description 25 | # # => "be bigger than 2 and smaller than 4" 26 | # ...rather than: 27 | # # => "be bigger than 2" 28 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 29 | end 30 | 31 | # rspec-mocks config goes here. You can use an alternate test double 32 | # library (such as bogus or mocha) by changing the `mock_with` option here. 33 | config.mock_with :rspec do |mocks| 34 | # Prevents you from mocking or stubbing a method that does not exist on 35 | # a real object. This is generally recommended, and will default to 36 | # `true` in RSpec 4. 37 | mocks.verify_partial_doubles = true 38 | end 39 | 40 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will 41 | # have no way to turn it off -- the option exists only for backwards 42 | # compatibility in RSpec 3). It causes shared context metadata to be 43 | # inherited by the metadata hash of host groups and examples, rather than 44 | # triggering implicit auto-inclusion in groups with matching metadata. 45 | config.shared_context_metadata_behavior = :apply_to_host_groups 46 | 47 | # The settings below are suggested to provide a good initial experience 48 | # with RSpec, but feel free to customize to your heart's content. 49 | =begin 50 | # This allows you to limit a spec run to individual examples or groups 51 | # you care about by tagging them with `:focus` metadata. When nothing 52 | # is tagged with `:focus`, all examples get run. RSpec also provides 53 | # aliases for `it`, `describe`, and `context` that include `:focus` 54 | # metadata: `fit`, `fdescribe` and `fcontext`, respectively. 55 | config.filter_run_when_matching :focus 56 | 57 | # Allows RSpec to persist some state between runs in order to support 58 | # the `--only-failures` and `--next-failure` CLI options. We recommend 59 | # you configure your source control system to ignore this file. 60 | config.example_status_persistence_file_path = "spec/examples.txt" 61 | 62 | # Limits the available syntax to the non-monkey patched syntax that is 63 | # recommended. For more details, see: 64 | # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ 65 | # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 66 | # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode 67 | config.disable_monkey_patching! 68 | 69 | # This setting enables warnings. It's recommended, but in some cases may 70 | # be too noisy due to issues in dependencies. 71 | config.warnings = true 72 | 73 | # Many RSpec users commonly either run the entire suite or an individual 74 | # file, and it's useful to allow more verbose output when running an 75 | # individual spec file. 76 | if config.files_to_run.one? 77 | # Use the documentation formatter for detailed output, 78 | # unless a formatter has already been configured 79 | # (e.g. via a command-line flag). 80 | config.default_formatter = "doc" 81 | end 82 | 83 | # Print the 10 slowest examples and example groups at the 84 | # end of the spec run, to help surface which specs are running 85 | # particularly slow. 86 | config.profile_examples = 10 87 | 88 | # Run specs in random order to surface order dependencies. If you find an 89 | # order dependency and want to debug it, you can fix the order by providing 90 | # the seed, which is printed after each run. 91 | # --seed 1234 92 | config.order = :random 93 | 94 | # Seed global randomization in this process using the `--seed` CLI option. 95 | # Setting this allows you to use `--seed` to deterministically reproduce 96 | # test failures related to randomization by passing the same `--seed` value 97 | # as the one that triggered the failure. 98 | Kernel.srand config.seed 99 | =end 100 | end 101 | -------------------------------------------------------------------------------- /qr-layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
