ips_qr_code 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +55 -0
- data/bin/ips-qr-code +87 -0
- data/lib/ips_qr_code/utils.rb +126 -0
- data/lib/ips_qr_code/version.rb +3 -0
- data/lib/ips_qr_code.rb +124 -0
- metadata +61 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 649400ee3e1c0ec1fac491f622e0affb57842d52d1935cd0c8bb7c76966729e4
|
4
|
+
data.tar.gz: d1ae5032032398103ddbb2fddf002b497526ee272f94e440fde9c215c06fcdb3
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 977c5c6c6b2be6910250e8462f68233f2694a503725d4f418bfa3c946b6b67c9c0146d37219f6e2032dcc85b2bce183f9f44e49ac0eb23d318a7234403187267
|
7
|
+
data.tar.gz: 69a37eb75a36c7ed5f6a29dd3a52ae5cbc9a51296666c2392eb0269f9eec711cfb0432983e9bf507f7e0ca21b19796690d6fa4d657e93263498e9502fce3cbb2
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2020 ArtBIT
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# IPS QR Code Generator (Ruby)
|
2
|
+
|
3
|
+
Gem to generate IPS QR code formatted strings.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'ips_qr_code'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle install
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install ips_qr_code
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
require 'ips_qr_code'
|
25
|
+
|
26
|
+
data = IpsQrCode.generate(
|
27
|
+
r: '845000000014284968', # racun primaoca
|
28
|
+
n: 'EPS Snabdevanje 11000 Beograd', # naziv primaoca
|
29
|
+
sf: '221', # sifra placanja
|
30
|
+
i: 'RSD1000,00' # iznos
|
31
|
+
# optional:
|
32
|
+
# k: 'PR', # kod
|
33
|
+
# v: '01', # verzija
|
34
|
+
# c: '1', # kodni raspored
|
35
|
+
# o: '200000000012345600', # racun platioca
|
36
|
+
# p: 'Marko Markovic', # naziv platioca
|
37
|
+
# s: 'Uplata po racunu', # svrha placanja
|
38
|
+
# m: '1234', # merchantCodeCategory
|
39
|
+
# js: '12345', # jednokratnaSifra
|
40
|
+
# ro: '97123456789', # pozivNaBroj
|
41
|
+
# rl: 'ref primaoca', # referencaPrimaoca
|
42
|
+
# rp: '1234567890123456789' # referencaPlacanja
|
43
|
+
)
|
44
|
+
puts data
|
45
|
+
```
|
46
|
+
|
47
|
+
### CLI
|
48
|
+
|
49
|
+
```bash
|
50
|
+
$ ips-qr-code --racun-primaoca 845000000014284968 \
|
51
|
+
--naziv-primaoca "EPS Snabdevanje 11000 Beograd" \
|
52
|
+
--sifra-placanja 221 \
|
53
|
+
--iznos RSD1000,00
|
54
|
+
# => K:PR|V:01|C:1|R:845000000014284968|N:EPS Snabdevanje...
|
55
|
+
```
|
data/bin/ips-qr-code
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
require 'ips_qr_code'
|
5
|
+
require 'rqrcode'
|
6
|
+
|
7
|
+
options = {}
|
8
|
+
to_file = nil
|
9
|
+
parser = OptionParser.new do |opts|
|
10
|
+
opts.banner = "Usage: ips-qr-code [options]"
|
11
|
+
|
12
|
+
opts.on("--kod K", "Kod (PR, PT, PK, EK)") { |v| options[:k] = v }
|
13
|
+
opts.on("--verzija V", "Verzija (01)") { |v| options[:v] = v }
|
14
|
+
opts.on("--kodni-raspored C", "Kod raspored (1)") { |v| options[:c] = v }
|
15
|
+
opts.on("--racun-primaoca R", "Broj racuna primaoca") { |v| options[:r] = v }
|
16
|
+
opts.on("--naziv-primaoca N", "Naziv primaoca") { |v| options[:n] = v }
|
17
|
+
opts.on("--racun-platioca O", "Broj racuna platioca") { |v| options[:o] = v }
|
18
|
+
opts.on("--naziv-platioca P", "Naziv platioca") { |v| options[:p] = v }
|
19
|
+
opts.on("--sifra-placanja SF", "Sifra placanja") { |v| options[:sf] = v }
|
20
|
+
opts.on("--iznos I", "Iznos (e.g. RSD1000,00)") { |v| options[:i] = v }
|
21
|
+
opts.on("--svrha-placanja S", "Svrha placanja") { |v| options[:s] = v }
|
22
|
+
opts.on("--merchant-code-category M", "Merchant code category") { |v| options[:m] = v }
|
23
|
+
opts.on("--jednokratna-sifra J", "Jednokratna sifra") { |v| options[:js] = v }
|
24
|
+
opts.on("--poziv-na-broj RO", "Poziv na broj") { |v| options[:ro] = v }
|
25
|
+
opts.on("--referenca-primaoca RL", "Referenca primaoca") { |v| options[:rl] = v }
|
26
|
+
opts.on("--referenca-placanja RP", "Referenca placanja") { |v| options[:rp] = v }
|
27
|
+
opts.on("--to-file FILE", "Save QR code to image file") { |v| to_file = v }
|
28
|
+
|
29
|
+
opts.on("-h", "--help", "Prints this help") do
|
30
|
+
puts opts
|
31
|
+
exit
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
begin
|
36
|
+
parser.parse!
|
37
|
+
result = IpsQrCode.generate(options)
|
38
|
+
|
39
|
+
if to_file
|
40
|
+
begin
|
41
|
+
qrcode = RQRCode::QRCode.new(result)
|
42
|
+
|
43
|
+
case File.extname(to_file).downcase
|
44
|
+
when '.png'
|
45
|
+
png = qrcode.as_png(
|
46
|
+
bit_depth: 1,
|
47
|
+
border_modules: 4,
|
48
|
+
color_mode: ChunkyPNG::COLOR_GRAYSCALE,
|
49
|
+
color: 'black',
|
50
|
+
file: nil,
|
51
|
+
fill: 'white',
|
52
|
+
module_px_size: 6,
|
53
|
+
resize_exactly_to: false,
|
54
|
+
resize_gte_to: false,
|
55
|
+
size: 300
|
56
|
+
)
|
57
|
+
IO.binwrite(to_file, png.to_s)
|
58
|
+
puts "QR code saved to #{to_file}"
|
59
|
+
when '.svg'
|
60
|
+
svg = qrcode.as_svg(
|
61
|
+
offset: 0,
|
62
|
+
color: '000',
|
63
|
+
shape_rendering: 'crispEdges',
|
64
|
+
module_size: 6,
|
65
|
+
standalone: true
|
66
|
+
)
|
67
|
+
File.write(to_file, svg)
|
68
|
+
puts "QR code saved to #{to_file}"
|
69
|
+
else
|
70
|
+
puts "Unsupported file format. Use .png or .svg"
|
71
|
+
end
|
72
|
+
rescue => e
|
73
|
+
warn "Error generating QR code: #{e.message}"
|
74
|
+
exit 1
|
75
|
+
end
|
76
|
+
else
|
77
|
+
puts result
|
78
|
+
end
|
79
|
+
|
80
|
+
rescue OptionParser::InvalidOption => e
|
81
|
+
warn e.message
|
82
|
+
puts parser
|
83
|
+
exit 1
|
84
|
+
rescue ArgumentError => e
|
85
|
+
warn "Error: #{e.message}"
|
86
|
+
exit 1
|
87
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
module IpsQrCode
|
2
|
+
module Utils
|
3
|
+
RENAME_KEYS = {
|
4
|
+
'kod' => :k,
|
5
|
+
'verzija' => :v,
|
6
|
+
'version' => :v,
|
7
|
+
'characterEncodingType' => :c,
|
8
|
+
'znakovniSkup' => :c,
|
9
|
+
'racunPrimaoca' => :r,
|
10
|
+
'nazivPrimaoca' => :n,
|
11
|
+
'iznos' => :i,
|
12
|
+
'nazivPlatioca' => :p,
|
13
|
+
'sifraPlacanja' => :sf,
|
14
|
+
'svrhaPlacanja' => :s,
|
15
|
+
'merchantCodeCategory' => :m,
|
16
|
+
'jednokratnaSifra' => :js,
|
17
|
+
'pozivNaBroj' => :ro,
|
18
|
+
'referencaPrimaoca' => :rl,
|
19
|
+
'referencaPlacanja' => :rp
|
20
|
+
}.freeze
|
21
|
+
|
22
|
+
def self.rename_keys(opts)
|
23
|
+
result = {}
|
24
|
+
opts.each do |key, value|
|
25
|
+
key_str = key.to_s
|
26
|
+
new_key = RENAME_KEYS[key_str] || key_str.to_sym
|
27
|
+
result[new_key] = value
|
28
|
+
end
|
29
|
+
result
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.normalize_bank_account(input)
|
33
|
+
numerals = input.to_s.gsub(/\D/, '')
|
34
|
+
bank = numerals[0,3]
|
35
|
+
control = numerals[-2,2] || numerals[-2..-1]
|
36
|
+
account = numerals[3...-2] || ''
|
37
|
+
"#{bank}#{account.rjust(13, '0')}#{control}"
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.modulo(divident, divisor)
|
41
|
+
div = divident.to_s
|
42
|
+
divisor = divisor.to_i
|
43
|
+
while div.length > 10
|
44
|
+
part = div[0,10].to_i
|
45
|
+
div = (part % divisor).to_s + div[10..-1]
|
46
|
+
end
|
47
|
+
div.to_i % divisor
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.calculate_bank_account_control_number(input)
|
51
|
+
98 - modulo(input[0...-2] + '00', 97)
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.validate_bank_account(input)
|
55
|
+
normalized = normalize_bank_account(input)
|
56
|
+
control_number = normalized[-2,2]
|
57
|
+
calculated_number = calculate_bank_account_control_number(normalized).to_s.rjust(2, '0')
|
58
|
+
control_number == calculated_number
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.validate_reference_number(input)
|
62
|
+
return true if input.nil? || input.to_s.empty?
|
63
|
+
input_str = input.to_s
|
64
|
+
model = input_str[0,2]
|
65
|
+
case model
|
66
|
+
when '97'
|
67
|
+
sanitized = sanitize97(input_str)
|
68
|
+
control = sanitized[2,2]
|
69
|
+
poziv = sanitized[4..-1]
|
70
|
+
control.to_i == mod97(poziv)
|
71
|
+
when '22'
|
72
|
+
sanitized = sanitize22(input_str)
|
73
|
+
poziv = sanitized[5...-1]
|
74
|
+
control = sanitized[-1]
|
75
|
+
control.to_i == mod22(poziv)
|
76
|
+
when '00'
|
77
|
+
true
|
78
|
+
else
|
79
|
+
warn "Model #{model} is not supported."
|
80
|
+
true
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.sanitize97(input)
|
85
|
+
input.to_s.upcase.gsub(/[ \-]/, '').chars.map.with_index do |char, idx|
|
86
|
+
code = char.ord
|
87
|
+
if code.between?(65, 90)
|
88
|
+
(code - 55).to_s
|
89
|
+
elsif code.between?(48, 57)
|
90
|
+
(code - 48).to_s
|
91
|
+
else
|
92
|
+
raise ArgumentError, "Invalid character \"#{char}\" at position #{idx}"
|
93
|
+
end
|
94
|
+
end.join
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.sanitize22(input)
|
98
|
+
input.to_s.gsub(/[ \-]/, '').chars.map.with_index do |char, idx|
|
99
|
+
code = char.ord
|
100
|
+
if code.between?(48, 57)
|
101
|
+
(code - 48).to_s
|
102
|
+
else
|
103
|
+
raise ArgumentError, "Invalid character \"#{char}\" at position #{idx}"
|
104
|
+
end
|
105
|
+
end.join
|
106
|
+
end
|
107
|
+
|
108
|
+
def self.mod97(input)
|
109
|
+
control = 0
|
110
|
+
base = 100
|
111
|
+
input.to_s.reverse.chars.map(&:to_i).each do |char|
|
112
|
+
control = (control + base * char) % 97
|
113
|
+
base = (base * 10) % 97
|
114
|
+
end
|
115
|
+
98 - control
|
116
|
+
end
|
117
|
+
|
118
|
+
def self.mod22(input)
|
119
|
+
control = input.to_s.reverse.chars.map(&:to_i).each_with_index.reduce(0) do |sum,(char, idx)|
|
120
|
+
sum + (idx + 1) * char
|
121
|
+
end
|
122
|
+
control = 11 - (control % 11)
|
123
|
+
control % 10
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
data/lib/ips_qr_code.rb
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
require 'ips_qr_code/version'
|
2
|
+
require 'ips_qr_code/utils'
|
3
|
+
|
4
|
+
module IpsQrCode
|
5
|
+
ORDER = %i[k v c r n i sf s m js ro rl rp].freeze
|
6
|
+
|
7
|
+
def self.generate(opts = {})
|
8
|
+
opts = Utils.rename_keys(opts)
|
9
|
+
opts = { k: 'PR', v: '01', c: '1' }.merge(opts)
|
10
|
+
|
11
|
+
# Validate required fields
|
12
|
+
%i[r n i sf].each do |field|
|
13
|
+
raise ArgumentError, "Missing required #{field}" unless opts[field]
|
14
|
+
end
|
15
|
+
|
16
|
+
# Normalize and validate racun primaoca (r)
|
17
|
+
opts[:r] = Utils.normalize_bank_account(opts[:r])
|
18
|
+
unless opts[:r] =~ /^\d{18}$/ && Utils.validate_bank_account(opts[:r])
|
19
|
+
raise ArgumentError, 'Invalid racunPrimaoca'
|
20
|
+
end
|
21
|
+
|
22
|
+
# Optional racun platioca (o)
|
23
|
+
if opts[:o]
|
24
|
+
opts[:o] = Utils.normalize_bank_account(opts[:o])
|
25
|
+
unless opts[:o] =~ /^\d{18}$/ && Utils.validate_bank_account(opts[:o])
|
26
|
+
raise ArgumentError, 'Invalid racunPlatioca'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Validate naziv primaoca (n)
|
31
|
+
n = opts[:n].to_s
|
32
|
+
if n.length < 1 || n.length > 70
|
33
|
+
raise ArgumentError, 'nazivPrimaoca must be between 1 and 70 characters'
|
34
|
+
end
|
35
|
+
unless n.match?(/^[a-zA-ZšđžčćŠĐŽČĆ0-9 \(\)\{\}\[\]<>\/\.,:;!@#\$%\^&\?„""""'`''_~=+\-\s]+$/u)
|
36
|
+
raise ArgumentError, 'Only letters, numbers, and select special characers are allowed.'
|
37
|
+
end
|
38
|
+
if n.lines.count > 3
|
39
|
+
raise ArgumentError, 'nazivPrimaoca must not contain more than 3 lines'
|
40
|
+
end
|
41
|
+
|
42
|
+
# Optional naziv platioca (p)
|
43
|
+
if opts[:p]
|
44
|
+
p = opts[:p].to_s
|
45
|
+
if p.length > 70
|
46
|
+
raise ArgumentError, 'nazivPlatioca must be at most 70 characters'
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Validate iznos (i)
|
51
|
+
i_val = opts[:i].to_s
|
52
|
+
unless i_val.length.between?(5, 20) && i_val.match?(/^[A-Z]{3}[0-9]+,[0-9]{0,2}$/)
|
53
|
+
raise ArgumentError, 'Invalid iznos format'
|
54
|
+
end
|
55
|
+
|
56
|
+
# Validate sifra placanja (sf)
|
57
|
+
sf = opts[:sf].to_s
|
58
|
+
unless sf.match?(/^[12][0-9]{2}$/)
|
59
|
+
raise ArgumentError, 'Invalid sifraPlacanja'
|
60
|
+
end
|
61
|
+
|
62
|
+
# Optional svrha placanja (s)
|
63
|
+
if opts[:s]
|
64
|
+
s = opts[:s].to_s
|
65
|
+
if s.length > 35
|
66
|
+
raise ArgumentError, 'svrhaPlacanja must be at most 35 characters'
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Optional merchant code category (m)
|
71
|
+
if opts[:m]
|
72
|
+
m = opts[:m].to_s
|
73
|
+
unless m.match?(/^[0-9]{4}$/)
|
74
|
+
raise ArgumentError, 'Invalid merchantCodeCategory'
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Optional jednokratna sifra (js)
|
79
|
+
if opts[:js]
|
80
|
+
js = opts[:js].to_s
|
81
|
+
unless js.match?(/^[0-9]{5}$/)
|
82
|
+
raise ArgumentError, 'Invalid jednokratnaSifra'
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Optional poziv na broj (ro)
|
87
|
+
if opts[:ro]
|
88
|
+
ro = opts[:ro].to_s
|
89
|
+
if ro.length > 35
|
90
|
+
raise ArgumentError, 'pozivNaBroj must be at most 35 characters'
|
91
|
+
end
|
92
|
+
ro = "00#{ro}" unless %w[97 22 11 00].include?(ro[0,2])
|
93
|
+
unless Utils.validate_reference_number(ro)
|
94
|
+
raise ArgumentError, 'Invalid pozivNaBroj'
|
95
|
+
end
|
96
|
+
opts[:ro] = ro
|
97
|
+
end
|
98
|
+
|
99
|
+
# Optional referenca primaoca (rl)
|
100
|
+
if opts[:rl]
|
101
|
+
rl = opts[:rl].to_s
|
102
|
+
if rl.length > 140
|
103
|
+
raise ArgumentError, 'referencaPrimaoca must be at most 140 characters'
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Optional referenca placanja (rp)
|
108
|
+
if opts[:rp]
|
109
|
+
rp = opts[:rp].to_s
|
110
|
+
unless rp.match?(/^[0-9]{19}$/)
|
111
|
+
raise ArgumentError, 'Invalid referencaPlacanja'
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Build IPS data string
|
116
|
+
parts = ORDER.map do |key|
|
117
|
+
val = opts[key]
|
118
|
+
next unless val
|
119
|
+
"#{key.to_s.upcase}:#{val.to_s.gsub('|', '-')}"
|
120
|
+
end.compact
|
121
|
+
|
122
|
+
parts.join('|')
|
123
|
+
end
|
124
|
+
end
|
metadata
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ips_qr_code
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ivan Nemytchenko
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 2025-04-20 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: rqrcode
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '2.0'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '2.0'
|
26
|
+
description: Generate IPS QR Code data strings
|
27
|
+
email:
|
28
|
+
- nemytchenko@gmail.com
|
29
|
+
executables:
|
30
|
+
- ips-qr-code
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- LICENSE
|
35
|
+
- README.md
|
36
|
+
- bin/ips-qr-code
|
37
|
+
- lib/ips_qr_code.rb
|
38
|
+
- lib/ips_qr_code/utils.rb
|
39
|
+
- lib/ips_qr_code/version.rb
|
40
|
+
homepage: https://github.com/inem/ips-qr-code
|
41
|
+
licenses:
|
42
|
+
- MIT
|
43
|
+
metadata: {}
|
44
|
+
rdoc_options: []
|
45
|
+
require_paths:
|
46
|
+
- lib
|
47
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: '0'
|
52
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '0'
|
57
|
+
requirements: []
|
58
|
+
rubygems_version: 3.6.4
|
59
|
+
specification_version: 4
|
60
|
+
summary: Generate IPS QR Code data strings
|
61
|
+
test_files: []
|