├── .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 |
4 |
5 |
Ricevuta
6 |
Conto / Pagabile a
7 |
8 | CH93 0076 2011 6238 5295 7
9 | Compagnia di assicurazione forma & scalciante
10 | Via cantonale 24
11 | 3000 Lugano 12 |
13 |

14 |
Referenza
15 |
16 | RF89 MTR8 1UUW ZYO4 8NY5 5NP3 17 |
18 |

19 |
Pagabile da
20 |
21 | Foobar Barfoot
22 | Via Cantonale 25
23 | 3001 Comano
24 |
25 | 26 |
27 |
28 | Valuta
29 | CHF 30 |
31 | 32 |
33 | Importo
34 | 2151.00 35 |
36 |
37 | 38 |
39 | Punto di accettazione 40 |
41 | 42 |
43 |
44 |
45 |
Sezione pagamento
46 |
47 |
48 |
49 | Valuta
50 | CHF 51 |
52 | 53 |
54 | Importo
55 | 2151.00 56 |
57 |
58 | 59 |
60 | Name AV1: UV;UltraPay005;12345 61 | Name AV2: XY;XYService;54321 62 |
63 |
64 |
65 |
Conto / Pagabile a
66 |
67 | CH93 0076 2011 6238 5295 7
68 | Compagnia di assicurazione forma & scalciante
69 | Via cantonale 24
70 | 3000 Lugano
71 |
72 |

73 |
Referenza
74 |
75 | RF89 MTR8 1UUW ZYO4 8NY5 5NP3 76 |
77 |

78 |
Informazioni supplementari
79 |
80 | Premio mensile luglio 2020 81 |
82 |

83 |
Pagabile da
84 |
85 | Foobar Barfoot
86 | Via Cantonale 25
87 | 3001 Lugano
88 |
89 |
90 |
91 |
92 | 93 | 207 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CircleCI](https://circleci.com/gh/damoiser/qr-bills/tree/master.svg?style=svg)](https://circleci.com/gh/damoiser/qr-bills/tree/master) 2 | ![](https://ruby-gem-downloads-badge.herokuapp.com/qr-bills?type=total) 3 | [![Gem Version](https://badge.fury.io/rb/qr-bills.svg)](https://badge.fury.io/rb/qr-bills) 4 | [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/donate?business=DJNJMV5YAEBT6¤cy_code=CHF) 5 | 6 | QR-Bills gem for implementing Swiss payments. 7 | 8 | ## Notes 9 | 10 | Please note that **no checks are performed to validate IBAN and references (like Creditor Reference)** when submitting the params. 11 | These checks are required to be performed by the application. 12 | 13 | ### Deprecation warnings ⚠️ 14 | :warning: :fire: If you are using `qrcode_filepath` please switch and start using `qrcode_format` as `qrcode_filepath` will be deprecated and removed! 15 | 16 | ## Installation 17 | 18 | ### Gem installation 19 | 20 | ```bash 21 | gem install qr-bills 22 | ``` 23 | * [Rubygems > qr-bills](https://rubygems.org/gems/qr-bills) 24 | 25 | ### Locales / Translations 26 | 27 | To support translations, copy/paste the 4 languages code into your I18n engine: `config/locales/*.yml` 28 | 29 | ## Usage 30 | 31 | ### Generate a creditor reference (ISO-11649) number 32 | 33 | The creditor reference is composed as follow: 34 | * RF + 2 check digits + reference 35 | 36 | Max reference length is 21 chars. 37 | 38 | ```ruby 39 | QRBills.create_creditor_reference("MTR81UUWZYO48NY55NP3") 40 | # => "RF89MTR81UUWZYO48NY55NP3" 41 | ``` 42 | 43 | When generating an invoice with a reference number where the creditor has an old (deprecated) ESR IBAN, the reference number must be of a different format. Refer to the comments above `QRBills.iban_type` for details. 44 | 45 | ### Availables outputs formats 46 | 47 | ```ruby 48 | params[:output_params][:format] = "html" 49 | # OR 50 | params[:output_params][:format] = "png" 51 | # OR 52 | params[:output_params][:format] = "svg" 53 | ``` 54 | 55 | * `html` returns a full qr-bill as a html-template string, uses `params[:qrcode_format]` for the qrcode format which supports `png` and `svg`. Defaults to `png`. 56 | * `png` returns the qrcode of the qr-bill as a ChunkyPNG::Image object. 57 | * `svg` returns the qrcode of the qr-bill as a svg string. 58 | 59 | ### Availables qr-bill types formats 60 | 61 | #### QR bill with (new) creditor reference 62 | 63 | This is a new bill type introduced with the new qr bills format 64 | ```ruby 65 | params[:bill_type] = QRBills.get_qrbill_with_creditor_reference_type 66 | params[:bill_params][:reference_type] = "SCOR" # fixed type for bill with creditor reference 67 | params[:bill_params][:reference] = "RF89MTR81UUWZYO48NY55NP3" # example 68 | ``` 69 | 70 | ![QR bill with creditor reference](./imgs/qr_bill_with_creditor_reference.png) 71 | 72 | #### QR bill with (old) reference type 73 | 74 | This can be compared to the (old) orange bill type 75 | ```ruby 76 | params[:bill_type] = QRBills.get_qrbill_with_qr_reference_type 77 | params[:bill_params][:reference_type] = "QRR" # fixed type for bill with qr reference 78 | params[:bill_params][:reference] = "00 00037 01588 13258 31366 09972" # example 79 | ``` 80 | 81 | ![QR bill with (old) reference type](./imgs/qr_bill_with_old_reference.png) 82 | 83 | #### QR bill without reference 84 | 85 | This can be compared to the (old) red bill type 86 | ```ruby 87 | params[:bill_type] = QRBills.get_qrbill_without_reference_type 88 | params[:bill_params][:reference_type] = "NON" # fixed type for bill without reference 89 | ``` 90 | 91 | ![QR bill without reference](./imgs/qr_bill_without_reference.png) 92 | 93 | ### Generate a QR-Bill 94 | 95 | ```ruby 96 | # get the QR params, so you will get the full hash structure and as well some default values 97 | params = QRBills.get_qr_params 98 | 99 | # fill the params, for example 100 | params[:bill_type] = QRBills.get_qrbill_with_creditor_reference_type 101 | params[:qrcode_filepath] = "#{Dir.pwd}/tmp/qrcode-html.png" 102 | params[:qrcode_format] = "svg" # use qrcode_format with "svg" / "png" instead of qrcode_filepath to use a data url encoded qr code 103 | params[:output_params][:format] = "html" 104 | params[:bill_params][:creditor][:iban] = "CH93 0076 2011 6238 5295 7" 105 | params[:bill_params][:creditor][:address][:type] = "S" # or type "K" 106 | params[:bill_params][:creditor][:address][:name] = "Compagnia di assicurazione forma & scalciante" 107 | params[:bill_params][:creditor][:address][:line1] = "Via cantonale" 108 | params[:bill_params][:creditor][:address][:line2] = "24" 109 | params[:bill_params][:creditor][:address][:postal_code] = "3000" 110 | params[:bill_params][:creditor][:address][:town] = "Lugano" 111 | params[:bill_params][:creditor][:address][:country] = "CH" 112 | params[:bill_params][:amount] = 12345.15 113 | params[:bill_params][:currency] = "CHF" 114 | params[:bill_params][:debtor][:address][:type] = "S" 115 | params[:bill_params][:debtor][:address][:name] = "Foobar Barfoot" 116 | params[:bill_params][:debtor][:address][:line1] = "Via cantonale" 117 | params[:bill_params][:debtor][:address][:line2] = "25" 118 | params[:bill_params][:debtor][:address][:postal_code] = "3001" 119 | params[:bill_params][:debtor][:address][:town] = "Comano" 120 | params[:bill_params][:debtor][:address][:country] = "CH" 121 | # you can get the new creditor reference using QRBills.create_creditor_reference("your_reference") 122 | params[:bill_params][:reference] = "RF89MTR81UUWZYO48NY55NP3" 123 | params[:bill_params][:reference_type] = "SCOR" 124 | params[:bill_params][:additionally_information] = "pagamento riparazione monopattino" 125 | 126 | # generate the QR Bill 127 | bill = QRBills.generate(params) 128 | 129 | # bill format is given in the params, default is html 130 | # bill has the following format: 131 | # bill = { 132 | # params: params, 133 | # output: "output" 134 | # } 135 | 136 | ``` 137 | 138 | ## References 139 | * https://www.paymentstandards.ch/dam/downloads/ig-qr-bill-en.pdf 140 | * https://www.paymentstandards.ch/en/shared/know-how/faq/qr.html 141 | * https://www.kmu.admin.ch/kmu/it/home/consigli-pratici/questioni-finanziarie/contabilita-e-revisione/introduzione-della-qr-fattura.html 142 | * https://www.paymentstandards.ch/dam/downloads/drehbuch-rechnung-steller-empfaenger-it.pdf 143 | 144 | ## TODO 145 | 146 | * add other outputs formats 147 | * add "empty" QR-Bill 148 | ![QR bill empty](./imgs/qr_bill_empty.png) 149 | 150 | ## License 151 | 152 | BSD-3 153 | -------------------------------------------------------------------------------- /spec/qr-params_spec.rb: -------------------------------------------------------------------------------- 1 | require 'i18n' 2 | require 'qr-bills/qr-params' 3 | 4 | RSpec.configure do |config| 5 | config.before(:each) do 6 | I18n.default_locale = :it 7 | 8 | @params = QRParams.get_qr_params 9 | @params[:bill_type] = QRParams::QR_BILL_WITH_QR_REFERENCE 10 | @params[:fonts][:eot] = "../web/assets/fonts/LiberationSans-Regular.eot" 11 | @params[:fonts][:woff] = "../web/assets/fonts/LiberationSans-Regular.woff" 12 | @params[:fonts][:ttf] = "../web/assets/fonts/LiberationSans-Regular.ttf" 13 | @params[:fonts][:svg] = "../web/assets/fonts/LiberationSans-Regular.svg" 14 | @params[:locales][:path] = "config/locales/" 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 | end 36 | end 37 | 38 | 39 | RSpec.describe "QR params" do 40 | it "uses current locale as language" do 41 | expect(@params[:bill_params][:language]).to be :it 42 | end 43 | 44 | describe "basic param validation" do 45 | it "fails if bill type is empty" do 46 | @params[:bill_type] = "" 47 | expect{QRParams.base_params_valid?(@params)}.to raise_error(ArgumentError, "QR-bill invalid parameters: bill type cannot be blank") 48 | end 49 | 50 | it "fails if bill type is nil" do 51 | @params[:bill_type] = nil 52 | expect{QRParams.base_params_valid?(@params)}.to raise_error(ArgumentError, "QR-bill invalid parameters: bill type cannot be blank") 53 | end 54 | 55 | it "fails if currency type is empty" do 56 | @params[:bill_params][:currency] = "" 57 | expect{QRParams.base_params_valid?(@params)}.to raise_error(ArgumentError, "QR-bill invalid parameters: currency cannot be blank") 58 | end 59 | 60 | it "fails if currency is nil" do 61 | @params[:bill_params][:currency] = nil 62 | expect{QRParams.base_params_valid?(@params)}.to raise_error(ArgumentError, "QR-bill invalid parameters: currency cannot be blank") 63 | end 64 | 65 | it "succeeds if the previous params are correctly set" do 66 | expect{QRParams.base_params_valid?(@params)}.not_to raise_error 67 | expect(QRParams.base_params_valid?(@params)).to be_truthy 68 | end 69 | end 70 | 71 | describe "qr bill with QR reference params validation" do 72 | it "fails if reference is not QRR" do 73 | @params[:bill_params][:reference_type]= "bla" 74 | expect{QRParams.qr_bill_with_qr_reference_valid?(@params)}.to raise_error(ArgumentError, "QR-bill invalid parameters: reference type must be 'QRR' for QR bill with standard reference") 75 | end 76 | 77 | it "fails if reference is empty" do 78 | @params[:bill_params][:reference_type]= "QRR" 79 | @params[:bill_params][:reference] = "" 80 | expect{QRParams.qr_bill_with_qr_reference_valid?(@params)}.to raise_error(ArgumentError, "QR-bill invalid parameters: reference cannot be blank for QR bill with standard reference") 81 | end 82 | 83 | it "fails if reference is nil" do 84 | @params[:bill_params][:reference_type]= "QRR" 85 | @params[:bill_params][:reference] = nil 86 | expect{QRParams.qr_bill_with_qr_reference_valid?(@params)}.to raise_error(ArgumentError, "QR-bill invalid parameters: reference cannot be blank for QR bill with standard reference") 87 | end 88 | 89 | it "succeeds if the previous checks pass" do 90 | @params[:bill_params][:reference_type]= "QRR" 91 | expect{QRParams.qr_bill_with_qr_reference_valid?(@params)}.not_to raise_error 92 | expect(QRParams.qr_bill_with_qr_reference_valid?(@params)).to be_truthy 93 | end 94 | end 95 | 96 | describe "qr bill with creditor reference params validation" do 97 | it "fails if reference is not SCOR" do 98 | @params[:bill_params][:reference_type]= "bla" 99 | expect{QRParams.qr_bill_with_creditor_reference_valid?(@params)}.to raise_error(ArgumentError, "QR-bill invalid parameters: reference type must be 'SCOR' for QR bill with (new) creditor reference") 100 | end 101 | 102 | it "fails if reference is empty" do 103 | @params[:bill_params][:reference_type]= "SCOR" 104 | @params[:bill_params][:reference] = "" 105 | expect{QRParams.qr_bill_with_creditor_reference_valid?(@params)}.to raise_error(ArgumentError, "QR-bill invalid parameters: reference cannot be blank for QR bill with (new) creditor reference") 106 | end 107 | 108 | it "fails if reference is nil" do 109 | @params[:bill_params][:reference_type]= "SCOR" 110 | @params[:bill_params][:reference] = nil 111 | expect{QRParams.qr_bill_with_creditor_reference_valid?(@params)}.to raise_error(ArgumentError, "QR-bill invalid parameters: reference cannot be blank for QR bill with (new) creditor reference") 112 | end 113 | 114 | it "succeeds if the previous checks pass" do 115 | @params[:bill_params][:reference_type]= "SCOR" 116 | expect{QRParams.qr_bill_with_creditor_reference_valid?(@params)}.not_to raise_error 117 | expect(QRParams.qr_bill_with_creditor_reference_valid?(@params)).to be_truthy 118 | end 119 | end 120 | 121 | describe "qr bill with without reference params validation" do 122 | it "fails if reference is not NON" do 123 | @params[:bill_params][:reference_type]= "bla" 124 | expect{QRParams.qr_bill_without_reference_valid?(@params)}.to raise_error(ArgumentError, "QR-bill invalid parameters: reference type must be 'NON' for QR bill without reference") 125 | end 126 | 127 | it "fails if reference has a value" do 128 | @params[:bill_params][:reference_type]= "NON" 129 | @params[:bill_params][:reference] = "bla" 130 | expect{QRParams.qr_bill_without_reference_valid?(@params)}.to raise_error(ArgumentError, "QR-bill invalid parameters: reference must be blank for QR bill without reference") 131 | end 132 | 133 | it "succeeds if the previous checks pass" do 134 | @params[:bill_params][:reference_type]= "NON" 135 | @params[:bill_params][:reference] = "" 136 | expect{QRParams.qr_bill_without_reference_valid?(@params)}.not_to raise_error 137 | expect(QRParams.qr_bill_without_reference_valid?(@params)).to be_truthy 138 | end 139 | end 140 | end 141 | -------------------------------------------------------------------------------- /lib/qr-bills/qr-generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rqrcode' 4 | 5 | module QRGenerator 6 | def self.create(params, qrcode_path = nil) 7 | format = params[:qrcode_format] || 'qrcode_png' 8 | 9 | case format 10 | when 'qrcode_png' 11 | build_qrcode_png(params[:bill_params], qrcode_path) 12 | when 'png' 13 | build_png(params[:bill_params]) 14 | when 'svg' 15 | build_svg(params[:bill_params]) 16 | else 17 | raise ArgumentError, "#{QRExceptions::NOT_SUPPORTED}: #{format} is not yet supported" 18 | end 19 | end 20 | 21 | def self.build_qrcode_png(bill_params, qrcode_path) 22 | warn('DEPRECATION WARNING: The qrcode_png format and qrcode_filepath parameter are deprecated and will be removed from qr-bills 1.1 (use png or svg instead)') 23 | 24 | final_qr = build_png(bill_params) 25 | final_qr.save(qrcode_path) 26 | final_qr 27 | end 28 | 29 | def self.build_png(bill_params) 30 | payload = build_payload(bill_params) 31 | qrcode = RQRCode::QRCode.new(payload, level: 'm') 32 | 33 | png = qrcode.as_png( 34 | bit_depth: 1, 35 | border_modules: 0, 36 | color_mode: ChunkyPNG::COLOR_GRAYSCALE, 37 | color: 'black', 38 | file: nil, 39 | fill: 'white', 40 | module_px_size: 10, 41 | resize_exactly_to: false, 42 | resize_gte_to: false, 43 | size: 1024 44 | ) 45 | 46 | swiss_cross = ChunkyPNG::Image.from_file(File.expand_path("#{File.dirname(__FILE__)}/../../web/assets/images/swiss_cross.png")) 47 | png.compose!(swiss_cross, (png.width - swiss_cross.width) / 2, (png.height - swiss_cross.height) / 2) 48 | end 49 | 50 | def self.build_svg(bill_params) 51 | payload = build_payload(bill_params) 52 | qrcode = RQRCode::QRCode.new(payload, level: 'm') 53 | 54 | # Generate qr code 55 | svg_code = qrcode.as_svg( 56 | standalone: true, # return a svg wrapper 57 | viewbox: true, # return a viewBox so we can extract it 58 | module_size: 10, 59 | use_path: true 60 | ) 61 | 62 | # Parse viewBox and calculate relative swiss cross size / center 63 | viewbox = svg_code.match(/viewBox="([\s\d]*)"/)[1] 64 | viewbox_size = viewbox.split(' ')[2].to_f 65 | swiss_cross_size = viewbox_size / (46.0 / 7.0) 66 | swiss_cross_center = (viewbox_size / 2.0) - (swiss_cross_size / 2.0) 67 | 68 | # Parse swiss cross and update size and coordinates 69 | svg_swiss_cross = File.open(File.expand_path("#{File.dirname(__FILE__)}/../../web/assets/images/swiss_cross.svg")).read 70 | svg_swiss_cross.gsub!('SWISS_CROSS_SIZE', swiss_cross_size.to_s) 71 | svg_swiss_cross.gsub!('SWISS_CROSS_CENTER', swiss_cross_center.to_s) 72 | 73 | # Assemble svg 74 | svg_code.gsub!(/\0') 75 | svg_code.gsub!(/<\/svg>/, "#{svg_swiss_cross}") 76 | 77 | # Optimize SVG for being URI-escaped 78 | # Stolen from Sprockets: https://github.com/rails/sprockets/blob/master/lib/sprockets/context.rb#L277-L293 79 | # 80 | # This only performs these basic but crucial optimizations: 81 | # * Removes comments, meta, doctype, and newlines. 82 | # * Replaces " with ', because ' does not need escaping. 83 | svg_code.gsub!(/|<\?.*?\?>|/m, '') 84 | svg_code.gsub!(/([\w:])="(.*?)"/, "\\1='\\2'") 85 | 86 | svg_code 87 | end 88 | 89 | # payload: 90 | # "SPC\r\n" + # indicator for swiss qr code: SPC (swiss payments code) 91 | # "0200\r\n" + # version of the specifications, 0200 = v2.0 92 | # "1\r\n" + # character set code: 1 = utf-8 restricted to the latin character set 93 | # "CH4431999123000889012\r\n" + # iban of the creditor (payable to) 94 | # "S\r\n" + # adress type: S = structured address, K = combined address elements (2 lines) 95 | # "Robert Schneider AG\r\n" + # creditor's name or company, max 70 characters 96 | # "Via Casa Postale\r\n" + # structured address: creditor's address street; combined address: address line 1 street and building number 97 | # "1268/2/22\r\n" + # structured address: creditor's building number; combined address: address line 2 including postal code and town 98 | # "2501\r\n" + # creditor's postal code 99 | # "Biel\r\n" + # creditor's town 100 | # "CH\r\n" + # creditor's country 101 | # "\r\n" + # optional: ultimate creditor's address type: S/K 102 | # "\r\n" + # optional: ultimate creditor's name/company 103 | # "\r\n" + # optional: ultimate creditor's street or address line 1 104 | # "\r\n" + # optional: ultimate creditor's building number or address line 2 105 | # "\r\n" + # optional: ultimate creditor's postal code 106 | # "\r\n" + # optional: ultimate creditor's town 107 | # "\r\n" + # optional: ultimate creditor's country 108 | # "123949.75\r\n" + # amount 109 | # "CHF\r\n" + # currency 110 | # "S\r\n"+ # debtor's address type (S/K) (payable by) 111 | # "Pia-Maria Rutschmann-Schnyder\r\n" + # debtor's name / company 112 | # "Grosse Marktgasse\r\n" + # debtor's street or address line 1 113 | # "28/5\r\n" + # debtor's building number or address line 2 114 | # "9400\r\n" + # debtor's postal code 115 | # "Rorschach\r\n" + # debtor's town 116 | # "CH\r\n" + # debtor's country 117 | # "QRR\r\n" + # reference type: QRR = QR reference, SCOR = Creditor reference, NON = without reference 118 | # "210000000003139471430009017\r\n" + # reference QR Reference: 27 chars check sum modulo 10 recursive, Creditor reference max 25 chars 119 | # "Beachten sie unsere Sonderangebotswoche bis 23.02.2017!\r\n" + # additional information unstructured message max 140 chars 120 | # "EPD\r\n" + # fixed indicator for EPD (end payment data) 121 | # "//S1/10/10201409/11/181105/40/0:30\r\n" + # bill information coded for automated booking of payment, data is not forwarded with the payment 122 | # "eBill/B/41010560425610173"; # alternative scheme paramaters, max 100 chars 123 | def self.build_payload(bill_params) 124 | payload = "SPC\r\n" 125 | payload += "0200\r\n" 126 | payload += "1\r\n" 127 | payload += "#{bill_params[:creditor][:iban].delete(' ')}\r\n" 128 | payload += "#{bill_params[:creditor][:address][:type]}\r\n" 129 | payload += "#{bill_params[:creditor][:address][:name]}\r\n" 130 | payload += "#{bill_params[:creditor][:address][:line1]}\r\n" 131 | payload += "#{bill_params[:creditor][:address][:line2]}\r\n" 132 | payload += "#{bill_params[:creditor][:address][:postal_code]}\r\n" 133 | payload += "#{bill_params[:creditor][:address][:town]}\r\n" 134 | payload += "#{bill_params[:creditor][:address][:country]}\r\n" 135 | payload += "\r\n" 136 | payload += "\r\n" 137 | payload += "\r\n" 138 | payload += "\r\n" 139 | payload += "\r\n" 140 | payload += "\r\n" 141 | payload += "\r\n" 142 | payload += "#{format('%.2f', bill_params[:amount])}\r\n" 143 | payload += "#{bill_params[:currency]}\r\n" 144 | payload += "#{bill_params[:debtor][:address][:type]}\r\n" 145 | payload += "#{bill_params[:debtor][:address][:name]}\r\n" 146 | payload += "#{bill_params[:debtor][:address][:line1]}\r\n" 147 | payload += "#{bill_params[:debtor][:address][:line2]}\r\n" 148 | payload += "#{bill_params[:debtor][:address][:postal_code]}\r\n" 149 | payload += "#{bill_params[:debtor][:address][:town]}\r\n" 150 | payload += "#{bill_params[:debtor][:address][:country]}\r\n" 151 | payload += "#{bill_params[:reference_type]}\r\n" 152 | payload += "#{bill_params[:reference].delete(' ')}\r\n" 153 | payload += "#{bill_params[:additionally_information]}\r\n" 154 | payload += "EPD\r\n" 155 | payload += "#{bill_params[:bill_information_coded]}\r\n" 156 | payload += "#{bill_params[:alternative_scheme_parameters]}\r\n" 157 | end 158 | end 159 | -------------------------------------------------------------------------------- /spec/fixtures/qrcode.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/qr-bills/qr-html-layout.rb: -------------------------------------------------------------------------------- 1 | require 'qr-bills/qr-generator' 2 | 3 | module QRHTMLLayout 4 | def self.create(params) 5 | qrcode = QRGenerator.create(params, params[:qrcode_filepath]) 6 | params[:qrcode_filepath] = convert_qrcode_to_data_url(qrcode) 7 | html_layout(params) 8 | end 9 | 10 | def self.convert_qrcode_to_data_url(qrcode) 11 | case qrcode 12 | when ChunkyPNG::Image 13 | qrcode.to_data_url 14 | else 15 | # Stolen from sprockets 16 | # https://github.com/rails/sprockets/blob/0f3e0e93dabafa8f3027e8036e40fd08902688c8/lib/sprockets/context.rb#L295-L303 17 | data = CGI.escape(qrcode) 18 | data.gsub!('%3D', '=') 19 | data.gsub!('%3A', ':') 20 | data.gsub!('%2F', '/') 21 | data.gsub!('%27', "'") 22 | data.tr!('+', ' ') 23 | 24 | "data:image/svg+xml;charset=utf-8,#{data}" 25 | end 26 | end 27 | 28 | def self.html_layout(params) 29 | I18n.with_locale(params[:bill_params][:language]) do 30 | layout = "
\n" 31 | layout += "
\n" 32 | layout += "
#{I18n.t("qrbills.receipt").capitalize}
\n" 33 | layout += "
#{I18n.t("qrbills.account").capitalize} / #{I18n.t("qrbills.payable_to").capitalize}
\n" 34 | layout += "
\n" 35 | layout += " #{params[:bill_params][:creditor][:iban]}
\n" 36 | layout += render_address(params[:bill_params][:creditor][:address]) 37 | layout += "
\n" 38 | layout += "

\n" 39 | 40 | if !params[:bill_params][:reference].nil? && !params[:bill_params][:reference].empty? 41 | layout += "
#{I18n.t("qrbills.reference").capitalize}
\n" 42 | layout += "
\n" 43 | layout += " #{params[:bill_params][:reference]}
\n" 44 | layout += "
\n" 45 | layout += "

\n" 46 | end 47 | 48 | layout += "
#{I18n.t("qrbills.payable_by").capitalize}
\n" 49 | layout += "
\n" 50 | layout += render_address(params[:bill_params][:debtor][:address]) 51 | layout += "
\n" 52 | 53 | layout += "
\n" 54 | layout += "
\n" 55 | layout += " #{I18n.t("qrbills.currency").capitalize}
\n" 56 | layout += " #{params[:bill_params][:currency]}
\n" 57 | layout += "
\n" 58 | 59 | layout += "
\n" 60 | layout += " #{I18n.t("qrbills.amount").capitalize}
\n" 61 | layout += " #{format('%.2f', params[:bill_params][:amount])}
\n" 62 | layout += "
\n" 63 | layout += "
\n" 64 | 65 | layout += "
\n" 66 | layout += " #{I18n.t("qrbills.acceptance_point").capitalize}
\n" 67 | layout += "
\n" 68 | 69 | layout += "
\n" 70 | layout += "
\n" 71 | layout += "
\n" 72 | layout += "
#{I18n.t("qrbills.payment_part").capitalize}
\n" 73 | layout += "
\n" 74 | layout += "
\n" 75 | layout += "
\n" 76 | layout += " #{I18n.t("qrbills.currency").capitalize}
\n" 77 | layout += " #{params[:bill_params][:currency]}
\n" 78 | layout += "
\n" 79 | 80 | layout += "
\n" 81 | layout += " #{I18n.t("qrbills.amount").capitalize}
\n" 82 | layout += " #{format('%.2f',params[:bill_params][:amount])}
\n" 83 | layout += "
\n" 84 | layout += "
\n" 85 | 86 | layout += "
\n" 87 | 88 | if !params[:bill_params][:bill_information_coded].nil? && !params[:bill_params][:bill_information_coded].empty? 89 | layout += " #{I18n.t("qrbills.name").capitalize} AV1: #{params[:bill_params][:bill_information_coded]}\n" 90 | end 91 | 92 | if !params[:bill_params][:alternative_scheme_parameters].nil? && !params[:bill_params][:alternative_scheme_parameters].empty? 93 | layout += " #{I18n.t("qrbills.name").capitalize} AV2: #{params[:bill_params][:alternative_scheme_parameters]}\n" 94 | end 95 | 96 | layout += "
\n" 97 | layout += "
\n" 98 | layout += "
\n" 99 | layout += "
#{I18n.t("qrbills.account").capitalize} / #{I18n.t("qrbills.payable_to").capitalize}
\n" 100 | layout += "
\n" 101 | layout += " #{params[:bill_params][:creditor][:iban]}
\n" 102 | layout += render_address(params[:bill_params][:creditor][:address]) 103 | layout += "
\n" 104 | layout += "

\n" 105 | 106 | if !params[:bill_params][:reference].nil? && !params[:bill_params][:reference].empty? 107 | layout += "
#{I18n.t("qrbills.reference").capitalize}
\n" 108 | layout += "
\n" 109 | layout += " #{params[:bill_params][:reference]}
\n" 110 | layout += "
\n" 111 | layout += "

\n" 112 | end 113 | 114 | if !params[:bill_params][:additionally_information].nil? && !params[:bill_params][:additionally_information].empty? 115 | layout += "
#{I18n.t("qrbills.additional_information").capitalize}
\n" 116 | layout += "
\n" 117 | layout += " #{params[:bill_params][:additionally_information]}
\n" 118 | layout += "
\n" 119 | layout += "

\n" 120 | end 121 | 122 | layout += "
#{I18n.t("qrbills.payable_by").capitalize}
\n" 123 | layout += "
\n" 124 | layout += render_address(params[:bill_params][:debtor][:address]) 125 | layout += "
\n" 126 | layout += "
\n" 127 | layout += "
\n" 128 | layout += "
\n" 129 | 130 | layout += "\n" 243 | 244 | layout 245 | end 246 | end 247 | 248 | def self.render_address(address) 249 | case address[:type] 250 | when 'S' 251 | format("%s
\n%s %s
\n%s %s
\n", address[:name], address[:line1], address[:line2], address[:postal_code], address[:town]) 252 | when 'K' 253 | format("%s
\n%s
\n%s
\n", address[:name], address[:line1], address[:line2]) 254 | end 255 | end 256 | end 257 | -------------------------------------------------------------------------------- /web/assets/fonts/LiberationSans-Regular.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | --------------------------------------------------------------------------------