upi 2.0.2 → 3.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 69ebd28c841dea94af52286485d067afc224f52523684d5bb3cbcacb9c79322a
4
- data.tar.gz: 4dad7fcf78afab8b327b01ee8310e2522181f76f4b8c0e3acb09b6d8f1c5ab4c
3
+ metadata.gz: 0c35150decd0ea8da417f11c7d5513cdf82a3609a61b071d9e41d6977ec0b802
4
+ data.tar.gz: d24b6754719ce81625612e38827d8a453df4d9a09b1a7d6966ff71c6972f0289
5
5
  SHA512:
6
- metadata.gz: 2e5e58ab3704147e279eda86fa89f2bcf40a5dafc0f5849a0ba91223265be59ec94e215f5dc104c8a5eca807f28f053c27a94b3dcd4a7fa4b67c7fb34c195d97
7
- data.tar.gz: 68e63cd763bba3a9db65ef0137466fe58462203befb6feaae7a983fc29863f425c8415953580108f045584ece2551b612050b8cb7ff253ba9559ff821d3b1d9e
6
+ metadata.gz: 5eba315c7f151124b1087ceba8555358fa5b95e3f07bc208e314b7f82f7497ea9617a9cab45bc7599a29574ef739b8e38a9067a68c3f10b9cf5aab100d9271f3
7
+ data.tar.gz: 7fe580edf651ff6eb855c66996fb7013121800a96ef5d8c8f6dc2a3690c9a14847d7cdf403ed3b78b6b59f59a005076de5b2e4b0d52c445486754e2db083f3c6
data/.rubocop.yml CHANGED
@@ -15,11 +15,16 @@ Layout/LineLength:
15
15
  Metrics/MethodLength:
16
16
  Max: 60
17
17
 
18
- Metrics/BlockLength:
19
- Max: 150
20
-
21
18
  Metrics/ParameterLists:
22
19
  Max: 15
23
20
 
24
21
  Metrics/AbcSize:
25
22
  Max: 20
23
+
24
+ Metrics/ClassLength:
25
+ Max: 250
26
+
27
+ Metrics/BlockLength:
28
+ Max: 150
29
+ Exclude:
30
+ - 'spec/**/*_spec.rb'
data/CHANGELOG.md CHANGED
@@ -1,6 +1,54 @@
1
1
  ## [Unreleased]
2
2
  - In the making
3
3
 
4
+ ## [3.0.0] - 2026-09-17
5
+
6
+ Correctness release. Earlier versions could emit a UPI URI that violated the NPCI
7
+ UPI Linking Specification. The QR code still scanned, so the only symptom was a
8
+ payment that failed in the payer's app, seemingly at random, depending on the
9
+ amount and note involved.
10
+
11
+ ### Fixed
12
+ - Spaces are now percent-encoded as `%20` instead of `+`. The specification's own
13
+ example URIs use `%20`; `+` is only correct for HTML form bodies, and PSP apps
14
+ that decode strictly showed names like `Test+User`.
15
+ - Amounts are normalised to exactly two decimal places. `1499.5` now emits
16
+ `am=1499.50`, and float representation noise (`0.1 + 0.2`) no longer emits
17
+ `am=0.30000000000000004`, which PSP apps reject.
18
+ - An amount with more than two decimal places, a zero amount, or a negative amount
19
+ now raises `Upi::ValidationError` instead of producing an unpayable QR code.
20
+ - `am` and `tn` are omitted entirely when not supplied. Previously every call
21
+ without an amount emitted `am=0&tn=`, which Google Pay and PhonePe reject.
22
+ - Values can no longer break out of the query string. A note containing `&` or `#`
23
+ used to truncate or forge parameters in `no_url_parse: false` mode.
24
+ - PNG output is sized by module rather than pinned to a 300px canvas, so a longer
25
+ payload produces a larger image instead of modules too small to scan reliably.
26
+ - `#upi_content` and `#generate_qr` no longer mutate the instance. `#params` is
27
+ frozen, calls no longer leak into one another, and instances can be shared.
28
+
29
+ ### Added
30
+ - Validation of every field against the specification: payee address format,
31
+ four-digit ISO 18245 merchant code, `INR`-only currency, 50-character note
32
+ limit, 35-character alphanumeric references, and `http(s)`-only URLs.
33
+ - `transaction_ref_id` is now required for merchant transactions carrying an
34
+ amount, as the specification mandates, and `Upi::Generator.generate_reference`
35
+ builds a unique one. A reused reference is rejected by PSPs as a duplicate,
36
+ which is why a QR with a hard-coded reference works once and then stops.
37
+ - `min_amount:` for the `mam` tag, which lets the payer edit the amount down to a
38
+ floor.
39
+ - `initiation_mode:` for the `mode` tag, with `MODE_QR` and friends. Opt-in.
40
+ - `level:` and renderer options on `#generate_qr`.
41
+ - `Upi::ValidationError`.
42
+
43
+ ### Changed
44
+ - Parameters are emitted in the order used by the specification's examples:
45
+ `pa, pn, mc, tid, tr, tn, am, mam, cu, url, mode`.
46
+ - `no_url_parse: false` now means "leave URL-safe punctuation readable" rather
47
+ than "do not escape anything"; separators are always escaped in both modes.
48
+ - `#params` returns the frozen constructor parameters, not the last call's state.
49
+ - QR error correction defaults to `:m` rather than rqrcode's `:h`, giving larger
50
+ modules at the same image size.
51
+
4
52
  ## [2.0.2] - 2024-09-08
5
53
  - For Individual Mode there need not be merchant code being sent. Removed default '0000'. Can be still added explicitly.
6
54
  - Changed the upi_content method to not parse the upi address and keep it as it is.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- upi (2.0.2)
4
+ upi (3.0.0)
5
5
  chunky_png
6
6
  rqrcode (= 2.2.0)
7
7
 
data/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # UPI
2
2
 
3
- The `upi` gem allows you to generate UPI QR codes for user-to-user payments. It supports both PNG and SVG formats for QR codes.
3
+ The `upi` gem generates UPI payment URIs and QR codes, in PNG or SVG, that conform to the
4
+ [NPCI UPI Linking Specification](https://www.npci.org.in/what-we-do/upi/product-overview)
5
+ (common URL specification for deep linking and proximity integration).
4
6
 
5
7
  ## Installation
6
8
 
@@ -20,14 +22,12 @@ Or install it yourself as:
20
22
 
21
23
  ## Usage
22
24
 
23
- Generating a QR Code
24
- To generate a UPI QR code, you need to initialize the Upi::Generator class with the required parameters and call the generate_qr method.
25
+ Initialize `Upi::Generator` with the payee details, then call `generate_qr` for a QR code or
26
+ `upi_content` for the raw payment URI. Generator instances are immutable and safe to reuse
27
+ and share across threads.
25
28
 
26
- ### Example
29
+ ### Individual mode (P2P)
27
30
 
28
- ## Individual Mode:
29
-
30
- ### Create a new UPI QR code generator instance
31
31
  ```ruby
32
32
  require 'upi'
33
33
 
@@ -35,117 +35,207 @@ generator = Upi::Generator.new(
35
35
  upi_id: 'test@upi',
36
36
  name: 'Test Name'
37
37
  )
38
+ ```
38
39
 
39
- # NOTE: For Individual Mode the Merchant Code is always 0000 which the code handles by default
40
+ Do not pass `merchant_code` for individual payments; it is omitted entirely.
40
41
 
41
- ```
42
+ #### QR code in SVG format
42
43
 
43
- ### Generate QR code in SVG format
44
44
  ```ruby
45
45
  svg_content = generator.generate_qr(100, 'Personal Payment', mode: :svg)
46
46
  File.write('qr_code.svg', svg_content)
47
47
  ```
48
48
 
49
- ### Generate QR code in PNG format
49
+ #### QR code in PNG format
50
+
50
51
  ```ruby
51
52
  png_content = generator.generate_qr(100, 'Personal Payment', mode: :png)
52
53
  File.binwrite('qr_code.png', png_content)
53
54
  ```
54
55
 
55
- ### Generating a Payment URL
56
- You can generate a UPI payment URL suitable for use in HTML links by using the generate_url method. This URL can be used as the href attribute in a "Pay Now" button or link.
56
+ #### Payment URI
57
+
58
+ `upi_content` returns a UPI URI you can use as the `href` of a "Pay Now" link:
57
59
 
58
60
  ```ruby
59
- # Generate UPI payment URL
60
61
  payment_url = generator.upi_content(100, 'Personal Payment')
61
- puts payment_url
62
-
63
- # The generate_url method returns a UPI URI string that can be used as a link in your HTML:
62
+ # => "upi://pay?pa=test@upi&pn=Test%20Name&tn=Personal%20Payment&am=100.00&cu=INR"
64
63
  ```
65
64
 
66
65
  ```html
67
66
  <a href="<%= payment_url %>">Pay Now</a>
68
67
  ```
69
68
 
70
- ## Merchant Mode:
69
+ #### Letting the payer choose the amount
70
+
71
+ Omit the amount and the `am` tag is left out, so the payer types their own. Never pass `0` for
72
+ this — UPI apps reject `am=0`, and the gem raises rather than emit it.
71
73
 
72
- ### Create a new UPI QR code generator instance
73
74
  ```ruby
74
- require 'upi'
75
+ generator.upi_content
76
+ # => "upi://pay?pa=test@upi&pn=Test%20Name&cu=INR"
77
+ ```
78
+
79
+ ### Merchant mode (P2M)
80
+
81
+ Pass a four-digit [ISO 18245](https://en.wikipedia.org/wiki/Merchant_category_code) merchant
82
+ category code:
75
83
 
84
+ ```ruby
76
85
  generator = Upi::Generator.new(
77
- upi_id: 'test@upi',
86
+ upi_id: 'merchant@upi',
78
87
  name: 'Test Name',
79
- merchant_code: '1234',
80
- currency: 'INR'
88
+ merchant_code: '5411'
81
89
  )
90
+ ```
82
91
 
83
- # NOTE: For Merchant Mode the Merchant Code needs to be set explicitly
92
+ A merchant payment that carries an amount **must** also carry a transaction reference (`tr`),
93
+ and that reference **must be unique for every payment attempt**. PSPs reject a reference they
94
+ have already seen as a duplicate, so a QR code with a hard-coded reference works once and then
95
+ silently stops. `Upi::Generator.generate_reference` builds a suitable one:
84
96
 
97
+ ```ruby
98
+ reference = Upi::Generator.generate_reference('ORD') # => "ORD20260917131656E4BBC91A"
99
+
100
+ svg_content = generator.generate_qr(
101
+ 500, 'Payment for Goods',
102
+ transaction_ref_id: reference,
103
+ transaction_id: 'TXN456',
104
+ url: 'https://merchant.com/payment',
105
+ mode: :svg
106
+ )
85
107
  ```
86
108
 
87
- ### Generate QR code in SVG format
109
+ In practice the reference is your own order or invoice number, which is what makes
110
+ reconciliation work:
111
+
88
112
  ```ruby
89
- svg_content = generator.generate_qr(500, 'Payment for Goods', transaction_ref_id: 'REF123', transaction_id: 'TXN456', url: 'https://merchant.com/payment', mode: :svg)
90
- File.write('qr_code_merchant.svg', svg_content)
113
+ generator.upi_content(500, 'Payment for Goods', transaction_ref_id: "ORD#{order.id}")
91
114
  ```
92
115
 
93
- ### Generate QR code in PNG format
116
+ Leaving the amount out produces a static "shop counter" QR, which needs no reference because
117
+ the payer supplies a fresh one each time:
118
+
94
119
  ```ruby
95
- png_content = generator.generate_qr(500, 'Payment for Goods', transaction_ref_id: 'REF123', transaction_id: 'TXN456', url: 'https://merchant.com/payment', mode: :png)
96
- File.binwrite('qr_code_merchant.png', png_content)
120
+ generator.upi_content
121
+ # => "upi://pay?pa=merchant@upi&pn=Test%20Name&mc=5411&cu=INR"
97
122
  ```
98
123
 
99
- ### Generating a Payment URL
100
- You can generate a UPI payment URL suitable for use in HTML links by using the generate_url method. This URL can be used as the href attribute in a "Pay Now" button or link.
124
+ ### Letting the payer pay at least a minimum
101
125
 
126
+ `min_amount` maps to the `mam` tag: the amount becomes editable, down to that floor.
102
127
 
103
128
  ```ruby
104
- # Generate UPI payment URL
105
- payment_url = generator.upi_content(500, 'Payment for Goods', transaction_ref_id: 'REF123', transaction_id: 'TXN456', url: 'https://merchant.com/payment')
106
- puts payment_url
107
-
108
- # The generate_url method returns a UPI URI string that can be used as a link in your HTML:
129
+ generator.upi_content(500, 'Donation', transaction_ref_id: 'ORD90211', min_amount: 100)
130
+ # => "upi://pay?pa=merchant@upi&pn=Test%20Name&mc=5411&tr=ORD90211&tn=Donation&am=500.00&mam=100.00&cu=INR"
109
131
  ```
110
132
 
111
- ```html
112
- <a href="<%= payment_url %>">Pay Now</a>
133
+ ## Parameters
134
+
135
+ ### `Upi::Generator.new`
136
+
137
+ | Parameter | Required | Notes |
138
+ | --- | --- | --- |
139
+ | `upi_id:` | yes | Payee VPA, `name@psp`. Max 255 characters. |
140
+ | `name:` | yes | Payee name shown on the payer's confirmation screen. Max 99 characters. |
141
+ | `currency:` | no | Defaults to `'INR'`, the only value UPI supports. |
142
+ | `merchant_code:` | no | Four-digit ISO 18245 category code. Its presence selects merchant mode. |
143
+ | `no_url_parse:` | no | Encoding strictness, see [Encoding](#encoding). Defaults to `true`. |
144
+
145
+ ### `#upi_content` and `#generate_qr`
146
+
147
+ Both take the same payment arguments; `generate_qr` adds the rendering ones.
148
+
149
+ | Parameter | Notes |
150
+ | --- | --- |
151
+ | `amount` | First positional. Omit to let the payer choose. Max two decimal places. |
152
+ | `note` | Second positional. Maps to `tn`. Max 50 characters. |
153
+ | `transaction_ref_id:` | `tr`. Mandatory for merchant payments with an amount. Max 35 alphanumeric characters. |
154
+ | `transaction_id:` | `tid`, PSP-generated when present. Max 35 alphanumeric characters. |
155
+ | `url:` | `http(s)` URL with further transaction detail. |
156
+ | `min_amount:` | `mam`. Makes the amount editable down to this floor. |
157
+ | `initiation_mode:` | `mode`. Opt-in, see [Initiation mode](#initiation-mode). |
158
+ | `mode:` | `generate_qr` only. `:svg` (default) or `:png`. |
159
+ | `level:` | `generate_qr` only. QR error correction: `:l`, `:m` (default), `:q`, `:h`. |
160
+
161
+ Any other keyword to `generate_qr` is passed through to the renderer, so
162
+ `module_px_size:`, `border_modules:`, `color:` and the rest of the
163
+ [rqrcode](https://github.com/whomwah/rqrcode) options still work:
164
+
165
+ ```ruby
166
+ generator.generate_qr(100, 'Tea', mode: :png, module_px_size: 12, color: '333')
113
167
  ```
114
168
 
115
- #### In the example above:
169
+ ## Amounts
116
170
 
117
- * Replace `'test@upi'` with the UPI ID of the recipient.
118
- * Replace `'Test Name'` with the recipient's name.
119
- * The `amount` parameter specifies the payment amount.
120
- * `note` is an optional field to include additional information.
121
- Parameters
122
- * upi_id: The UPI ID of the recipient.
123
- * name: The name of the recipient.
124
- * amount: The amount for the payment (required for UPI transactions).
125
- * currency: Currency code (default is 'INR').
126
- * note: Additional note or description for the payment.
127
- * merchant_code: Optional merchant code.
128
- * transaction_ref_id: Optional transaction reference ID.
129
- * transaction_id: Optional transaction ID.
130
- * url: Optional URL for additional information or payment redirect.
171
+ Amounts are normalised to the two-decimal format UPI requires, and anything that cannot be
172
+ represented that way raises rather than producing an unpayable QR code.
131
173
 
132
- #### Handling PNG and SVG
133
- You can specify the format of the QR code by using the mode parameter:
174
+ ```ruby
175
+ generator.upi_content(100) # => "...&am=100.00&..."
176
+ generator.upi_content(1499.5) # => "...&am=1499.50&..."
177
+ generator.upi_content(0.1 + 0.2) # => "...&am=0.30&..." float noise is discarded
178
+ generator.upi_content(BigDecimal('99.9'))
134
179
 
135
- * mode: :svg generates an SVG format QR code.
136
- * mode: :png generates a PNG format QR code.
180
+ generator.upi_content(100.456) # raises Upi::ValidationError, more than two decimals
181
+ generator.upi_content(0) # raises Upi::ValidationError, UPI apps reject am=0
182
+ ```
137
183
 
138
- ## No URI parse mode
184
+ Pass a `String` or `BigDecimal` when the amount comes from a currency column, so no float ever
185
+ touches it.
139
186
 
140
- If you want to generate a QR code without parsing the UPI address, you can use the no_uri_parse option:
187
+ ## Validation
188
+
189
+ Every field is checked against the specification before a URI is built, and a failure raises
190
+ `Upi::ValidationError` (a subclass of `Upi::Error`). This is deliberate: an invalid UPI URI
191
+ still produces a perfectly scannable QR code, so without validation the only symptom is a
192
+ payment that fails later, in the payer's app.
141
193
 
142
194
  ```ruby
143
- svg_content = generator.generate_qr(100, 'Personal Payment', no_uri_parse: true, mode: :svg)
144
- uri_content = generator.upi_content(100, 'Personal Payment', no_uri_parse: true)
195
+ Upi::Generator.new(upi_id: 'not-a-vpa', name: 'X')
196
+ # Upi::ValidationError: upi_id "not-a-vpa" is not a valid UPI address (expected 'name@psp')
197
+ ```
145
198
 
146
- # The no_uri_parse option will generate the QR code without URI parsing the UPI address.
199
+ ## Encoding
200
+
201
+ Values are percent-encoded per RFC 3986, with spaces as `%20`, matching the example URIs in
202
+ the specification. `no_url_parse` selects how aggressive that is:
203
+
204
+ * `true` (default) encodes down to the unreserved set.
205
+ * `false` leaves URL-safe punctuation such as `:` and `/` readable.
206
+
207
+ Either way the characters that would corrupt the query string — `&`, `#`, `%`, `+` and space —
208
+ are always escaped, so a note like `Tea & Coffee #12` can never truncate the URI or forge
209
+ another parameter. Redirect URLs keep their `://` readable in both modes.
210
+
211
+ ## Initiation mode
212
+
213
+ The specification lists a `mode` tag (`01` for QR, `04` for intent). It is left out by default,
214
+ since most PSP apps accept a URI without it and existing integrations rely on that. Opt in when
215
+ you want it:
216
+
217
+ ```ruby
218
+ generator.upi_content(500, 'Goods', transaction_ref_id: 'ORD1',
219
+ initiation_mode: Upi::Generator::MODE_QR)
220
+ # => "...&cu=INR&mode=01"
147
221
  ```
148
222
 
223
+ Signed QR codes (`mode=02`, requiring the `sign` and `orgid` tags) are not supported.
224
+
225
+ ## Upgrading from 2.x
226
+
227
+ 3.0 changes the generated URI so that it complies with the specification. See the
228
+ [CHANGELOG](CHANGELOG.md) for the full list. The changes most likely to affect you:
229
+
230
+ * Spaces encode as `%20`, not `+`, and parameters are emitted in the specification's order, so
231
+ any test asserting an exact URI string needs updating.
232
+ * Amounts always carry two decimals: `am=100` is now `am=100.00`.
233
+ * Calling without an amount no longer emits `am=0&tn=`; both tags are omitted.
234
+ * Invalid input now raises `Upi::ValidationError` instead of silently producing a broken QR
235
+ code. Merchant payments with an amount now require `transaction_ref_id`.
236
+ * `#params` returns the frozen constructor parameters rather than the last call's state.
237
+ * PNG output is sized by module rather than pinned to 300px, so images vary in size with the
238
+ payload. Pass `size:` or `module_px_size:` to control it.
149
239
 
150
240
  ## Development
151
241
 
@@ -155,7 +245,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
155
245
 
156
246
  ## Contributing
157
247
 
158
- Bug reports and pull requests are welcome on GitHub at https://github.com/stingrayzboy/upi. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/upi/blob/master/CODE_OF_CONDUCT.md).
248
+ Bug reports and pull requests are welcome on GitHub at https://github.com/stingrayzboy/upi. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/stingrayzboy/upi/blob/master/CODE_OF_CONDUCT.md).
159
249
 
160
250
  ## License
161
251
 
data/lib/upi/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Upi
4
- VERSION = '2.0.2'
4
+ VERSION = '3.0.0'
5
5
  end
data/lib/upi.rb CHANGED
@@ -2,118 +2,366 @@
2
2
 
3
3
  require 'rqrcode'
4
4
  require 'chunky_png'
5
- require 'uri'
5
+ require 'bigdecimal'
6
+ require 'securerandom'
6
7
  require_relative 'upi/version'
7
8
 
8
9
  module Upi
9
- # The Generator class is responsible for creating UPI QR codes and payment URLs.
10
+ # Base error class for the gem.
11
+ class Error < StandardError; end
12
+
13
+ # Raised when the supplied payment details cannot produce a spec-compliant UPI URI.
10
14
  #
11
- # You can generate a QR code for a UPI payment in either SVG or PNG format,
12
- # and also create a UPI payment URL for use in HTML links.
15
+ # Failing loudly at generation time is deliberate: an invalid UPI URI still
16
+ # produces a perfectly scannable QR code, so the only symptom is a payment
17
+ # that fails in the payer's app, long after the QR was handed out.
18
+ class ValidationError < Error; end
19
+
20
+ # Builds UPI payment URIs and QR codes that conform to the NPCI "UPI Linking
21
+ # Specifications" (common URL specification for deep linking and proximity
22
+ # integration, v1.6).
13
23
  #
14
- # Example usage:
24
+ # The canonical example from that document is the shape this class targets:
15
25
  #
16
- # generator = Upi::Generator.new(
17
- # upi_id: 'test@upi',
18
- # name: 'Test Name'
19
- # )
26
+ # upi://pay?pa=nadeem@npci&pn=nadeem%20chinna&mc=0000&tid=...&tr=...
27
+ # &tn=Pay%20to%20mystar%20store&am=10&mam=null&cu=INR&url=https://...
20
28
  #
21
- # svg_content = generator.generate_qr(100, 'Personal Payment', mode: :svg)
22
- # png_content = generator.generate_qr(100, 'Personal Payment', mode: :png)
23
- # payment_url = generator.upi_content(100, 'Personal Payment')
29
+ # Example usage:
24
30
  #
25
- # Parameters:
26
- # - `upi_id`: The UPI ID of the recipient.
27
- # - `name`: The name of the recipient.
28
- # - `amount`: The payment amount (required for UPI transactions).
29
- # - `currency`: The currency code (default is 'INR').
30
- # - `note`: An optional note or description for the payment.
31
- # - `merchant_code`: An optional merchant code.
32
- # - `transaction_ref_id`: An optional transaction reference ID.
33
- # - `transaction_id`: An optional transaction ID.
34
- # - `url`: An optional URL for additional information or payment redirect.
31
+ # generator = Upi::Generator.new(upi_id: 'test@upi', name: 'Test Name')
32
+ # svg = generator.generate_qr(100, 'Personal Payment', mode: :svg)
33
+ # png = generator.generate_qr(100, 'Personal Payment', mode: :png)
34
+ # uri = generator.upi_content(100, 'Personal Payment')
35
35
  #
36
- # @example
37
- # generator = Upi::Generator.new(
38
- # upi_id: 'test@upi',
39
- # name: 'Test Name'
40
- # )
41
- # svg_content = generator.generate_qr(100, 'Personal Payment', mode: :svg)
42
- # File.write('qr_code.svg', svg_content)
36
+ # Instances are immutable and safe to share between threads.
43
37
  #
44
- # @see https://www.npci.org.in/what-we-do/upi/product-overview for more details on UPI protocol
38
+ # @see https://www.npci.org.in/what-we-do/upi/product-overview
45
39
  class Generator
40
+ # Currently the only currency UPI supports.
41
+ CURRENCY = 'INR'
42
+
43
+ # Transaction initiation modes from the specification's `mode` tag.
44
+ MODE_DEFAULT = '00'
45
+ MODE_QR = '01'
46
+ MODE_SECURE_QR = '02'
47
+ MODE_INTENT = '04'
48
+
49
+ # Field limits. `pa`/`pn` follow the NPCI field sizes; `tn` is the widely
50
+ # enforced 50-character transaction note limit; `tr`/`tid` are kept inside
51
+ # the shortest limit PSPs are known to apply.
52
+ MAX_LENGTHS = { pa: 255, pn: 99, tn: 50, tr: 35, tid: 35 }.freeze
53
+
54
+ # A virtual payment address: `local@psp`.
55
+ VPA_PATTERN = /\A[A-Za-z0-9][A-Za-z0-9._-]{0,}@[A-Za-z][A-Za-z0-9.-]{0,}\z/.freeze
56
+
57
+ # Merchant category code, ISO 18245: exactly four digits.
58
+ MERCHANT_CODE_PATTERN = /\A\d{4}\z/.freeze
59
+
60
+ # Reference and transaction ids must be alphanumeric with no spaces.
61
+ REFERENCE_PATTERN = /\A[A-Za-z0-9._-]+\z/.freeze
62
+
63
+ NUMERIC_PATTERN = /\A\d+(\.\d+)?\z/.freeze
64
+
65
+ # Largest amount we will emit. Per-transaction ceilings are set by the bank
66
+ # and the merchant category, so this only catches obviously bad input.
67
+ MAX_AMOUNT = BigDecimal('10000000000')
68
+
69
+ # RFC 3986 unreserved set. Everything else is percent-encoded, which is what
70
+ # makes a space `%20` rather than `+`.
71
+ STRICT_UNSAFE = /[^A-Za-z0-9\-._~]/.freeze
72
+
73
+ # Leaves characters that are legal inside a query value untouched, so a
74
+ # redirect URL stays readable as `https://host/path` the way the NPCI
75
+ # examples show it. `&`, `#`, `%`, `+` and space are still escaped, so a
76
+ # value can never break out and forge another parameter.
77
+ LENIENT_UNSAFE = %r{[^A-Za-z0-9\-._~:/?=@!$'()*,;]}.freeze
78
+
79
+ # Emission order follows the example URIs in the specification.
80
+ PARAM_ORDER = %i[pa pn mc tid tr tn am mam cu url mode].freeze
81
+
82
+ # Renderer options accepted by rqrcode, used to reject a mistyped payment
83
+ # keyword rather than let it fall through to the renderer and quietly drop a
84
+ # field from the QR code.
85
+ SVG_OPTIONS = %i[fill use_path offset color shape_rendering module_size standalone
86
+ viewbox svg_attributes].freeze
87
+ PNG_OPTIONS = %i[bit_depth border_modules color_mode color file fill module_px_size
88
+ resize_exactly_to resize_gte_to size].freeze
89
+
46
90
  attr_reader :params, :no_url_parse
47
91
 
48
- def initialize(upi_id:, name:, currency: 'INR', merchant_code: nil, no_url_parse: true)
49
- @params = {
50
- pa: upi_id,
51
- pn: name,
52
- am: '',
53
- cu: currency,
54
- tn: '',
55
- mc: merchant_code,
56
- tr: nil,
57
- tid: nil,
58
- url: nil
59
- }.compact
92
+ # @param upi_id [String] payee VPA, e.g. `'merchant@upi'`
93
+ # @param name [String] payee name shown on the payer's confirmation screen
94
+ # @param currency [String] ISO 4217 code; UPI only supports `'INR'`
95
+ # @param merchant_code [String, nil] four-digit ISO 18245 merchant category
96
+ # code. Supplying it puts the generator in merchant (P2M) mode.
97
+ # @param no_url_parse [Boolean] `true` (default) percent-encodes values down
98
+ # to the RFC 3986 unreserved set; `false` leaves URL-safe punctuation
99
+ # readable. Both modes always escape the characters that would otherwise
100
+ # corrupt the query string.
101
+ def initialize(upi_id:, name:, currency: CURRENCY, merchant_code: nil, no_url_parse: true)
102
+ @upi_id = validate_vpa!(upi_id)
103
+ @name = validate_text!(name, :pn, 'name')
104
+ @currency = validate_currency!(currency)
105
+ @merchant_code = validate_merchant_code!(merchant_code)
60
106
  @no_url_parse = no_url_parse
107
+ @params = { pa: @upi_id, pn: @name, mc: @merchant_code, cu: @currency }.compact.freeze
61
108
  end
62
109
 
63
- def generate_qr(amount = '0', note = '', transaction_ref_id: nil, transaction_id: nil, url: nil, mode: :svg)
64
- content = upi_content(amount, note, transaction_ref_id: transaction_ref_id, transaction_id: transaction_id, url: url)
65
- qrcode = RQRCode::QRCode.new(content)
66
- generate_transaction(amount, note, transaction_id, transaction_ref_id, url)
67
-
68
- case mode
69
- when :svg
70
- qrcode.as_svg(
71
- color: '000',
72
- shape_rendering: 'crispEdges',
73
- module_size: 11
74
- )
75
- when :png
76
- png = qrcode.as_png(
77
- bit_depth: 1,
78
- border_modules: 4,
79
- color_mode: ChunkyPNG::COLOR_GRAYSCALE,
80
- color: 'black',
81
- file: nil,
82
- fill: 'white',
83
- module_px_size: 6, # Adjust size as needed
84
- resize_exactly_to: false,
85
- resize_gte_to: false,
86
- size: 300 # Adjust size as needed
87
- )
88
- png.to_s
110
+ # Generates a unique transaction reference suitable for the `tr` field.
111
+ #
112
+ # A `tr` must be unique per payment attempt: PSPs reject a repeat of one
113
+ # they have already seen, which is why a QR carrying a hard-coded reference
114
+ # works once and then quietly stops.
115
+ #
116
+ # @param prefix [String] alphanumeric prefix to make references recognisable
117
+ # @return [String]
118
+ def self.generate_reference(prefix = 'UPI')
119
+ raise ValidationError, "reference prefix #{prefix.inspect} must be alphanumeric" unless prefix.to_s.match?(REFERENCE_PATTERN)
120
+
121
+ "#{prefix}#{Time.now.strftime("%Y%m%d%H%M%S")}#{SecureRandom.hex(4).upcase}"
122
+ end
123
+
124
+ # Builds the `upi://pay?...` URI.
125
+ #
126
+ # @param amount [Numeric, String, nil] payment amount. Omit it (or pass nil)
127
+ # to let the payer type an amount; UPI apps reject `am=0`.
128
+ # @param note [String, nil] transaction note, max 50 characters
129
+ # @param transaction_ref_id [String, nil] `tr`. Mandatory for merchant
130
+ # transactions carrying an amount, and must be unique per attempt.
131
+ # @param transaction_id [String, nil] `tid`, PSP-generated when present
132
+ # @param url [String, nil] `http`/`https` transaction detail URL
133
+ # @param min_amount [Numeric, String, nil] `mam`. When given, the payer can
134
+ # edit the amount down to this floor.
135
+ # @param initiation_mode [String, nil] `mode` tag, e.g. {MODE_QR}
136
+ # @return [String]
137
+ def upi_content(amount = nil, note = nil, transaction_ref_id: nil, transaction_id: nil,
138
+ url: nil, min_amount: nil, initiation_mode: nil)
139
+ fields = build_fields(amount, note, transaction_ref_id, transaction_id, url,
140
+ min_amount, initiation_mode)
141
+ query = PARAM_ORDER.map do |key|
142
+ next if key == :pa
143
+
144
+ value = fields[key]
145
+ "#{key}=#{encode(value, lenient: key == :url)}" unless value.nil?
146
+ end.compact
147
+
148
+ "upi://pay?pa=#{fields[:pa]}&#{query.join("&")}"
149
+ end
150
+
151
+ # Renders the payment URI as a QR code.
152
+ #
153
+ # @param mode [Symbol] `:svg` (default) or `:png` output format
154
+ # @param level [Symbol] QR error correction level, `:l`, `:m`, `:q` or `:h`
155
+ # @param render_options [Hash] passed through to the rqrcode renderer,
156
+ # overriding the defaults (e.g. `module_px_size:`, `module_size:`)
157
+ # @return [String] SVG markup, or raw PNG bytes
158
+ # @see #upi_content for the payment parameters
159
+ def generate_qr(amount = nil, note = nil, transaction_ref_id: nil, transaction_id: nil,
160
+ url: nil, min_amount: nil, initiation_mode: nil,
161
+ mode: :svg, level: :m, **render_options)
162
+ raise ArgumentError, "Unsupported mode: #{mode}. Use :svg or :png." unless %i[svg png].include?(mode)
163
+
164
+ validate_render_options!(render_options, mode)
165
+ content = upi_content(amount, note, transaction_ref_id: transaction_ref_id,
166
+ transaction_id: transaction_id, url: url,
167
+ min_amount: min_amount, initiation_mode: initiation_mode)
168
+ qrcode = RQRCode::QRCode.new(content, level: level)
169
+
170
+ mode == :svg ? render_svg(qrcode, render_options) : render_png(qrcode, render_options)
171
+ end
172
+
173
+ private
174
+
175
+ def build_fields(amount, note, transaction_ref_id, transaction_id, url, min_amount, initiation_mode)
176
+ normalized_amount = normalize_amount(amount, 'amount')
177
+ reference = validate_reference!(transaction_ref_id, :tr, 'transaction_ref_id')
178
+ require_reference!(reference, normalized_amount)
179
+
180
+ params.merge(
181
+ tid: validate_reference!(transaction_id, :tid, 'transaction_id'),
182
+ tr: reference,
183
+ tn: presence(validate_text!(note, :tn, 'note')),
184
+ am: normalized_amount,
185
+ mam: normalize_min_amount(min_amount, normalized_amount),
186
+ url: validate_url!(url),
187
+ mode: validate_initiation_mode!(initiation_mode)
188
+ )
189
+ end
190
+
191
+ # The specification marks `tr` mandatory for merchant transactions and for
192
+ # dynamic (amount-bearing) URLs. Without it a PSP's risk engine sees an
193
+ # amount-carrying merchant payload with nothing to reconcile against, and
194
+ # rejects it behind a generic error.
195
+ def require_reference!(reference, amount)
196
+ return if reference || @merchant_code.nil? || amount.nil?
197
+
198
+ raise ValidationError,
199
+ 'transaction_ref_id is mandatory for merchant transactions with an amount, ' \
200
+ 'and must be unique per payment attempt. ' \
201
+ 'Use Upi::Generator.generate_reference to build one.'
202
+ end
203
+
204
+ def validate_vpa!(upi_id)
205
+ value = upi_id.to_s.strip
206
+ raise ValidationError, 'upi_id is required' if value.empty?
207
+
208
+ raise ValidationError, "upi_id must be at most #{MAX_LENGTHS[:pa]} characters" if value.length > MAX_LENGTHS[:pa]
209
+ raise ValidationError, "upi_id #{upi_id.inspect} is not a valid UPI address (expected 'name@psp')" unless value.match?(VPA_PATTERN)
210
+
211
+ value
212
+ end
213
+
214
+ def validate_text!(text, key, label)
215
+ return nil if text.nil?
216
+
217
+ value = text.to_s.tr("\r\n\t", ' ').strip
218
+ if value.length > MAX_LENGTHS[key]
219
+ raise ValidationError,
220
+ "#{label} must be at most #{MAX_LENGTHS[key]} characters, got #{value.length}"
221
+ end
222
+ raise ValidationError, "#{label} is required" if key == :pn && value.empty?
223
+
224
+ value
225
+ end
226
+
227
+ def validate_currency!(currency)
228
+ value = currency.to_s.strip.upcase
229
+ raise ValidationError, "currency #{currency.inspect} is not supported; UPI only supports 'INR'" unless value == CURRENCY
230
+
231
+ value
232
+ end
233
+
234
+ def validate_merchant_code!(merchant_code)
235
+ return nil if merchant_code.nil?
236
+
237
+ value = merchant_code.to_s.strip
238
+ unless value.match?(MERCHANT_CODE_PATTERN)
239
+ raise ValidationError,
240
+ "merchant_code #{merchant_code.inspect} must be a four-digit ISO 18245 category code"
241
+ end
242
+
243
+ value
244
+ end
245
+
246
+ def validate_reference!(reference, key, label)
247
+ return nil if reference.nil?
248
+
249
+ value = reference.to_s.strip
250
+ return nil if value.empty?
251
+
252
+ raise ValidationError, "#{label} must be at most #{MAX_LENGTHS[key]} characters, got #{value.length}" if value.length > MAX_LENGTHS[key]
253
+ raise ValidationError, "#{label} #{reference.inspect} must be alphanumeric with no spaces" unless value.match?(REFERENCE_PATTERN)
254
+
255
+ value
256
+ end
257
+
258
+ def validate_url!(url)
259
+ return nil if url.nil?
260
+
261
+ value = url.to_s.strip
262
+ return nil if value.empty?
263
+ raise ValidationError, "url #{url.inspect} must start with http:// or https://" unless value.start_with?('http://', 'https://')
264
+
265
+ value
266
+ end
267
+
268
+ def validate_initiation_mode!(initiation_mode)
269
+ return nil if initiation_mode.nil?
270
+
271
+ value = initiation_mode.to_s
272
+ raise ValidationError, "initiation_mode #{initiation_mode.inspect} must be two digits" unless value.match?(/\A\d{2}\z/)
273
+
274
+ value
275
+ end
276
+
277
+ def normalize_min_amount(min_amount, amount)
278
+ normalized = normalize_amount(min_amount, 'min_amount')
279
+ return nil if normalized.nil?
280
+
281
+ raise ValidationError, "min_amount #{normalized} cannot exceed amount #{amount}" if amount && BigDecimal(normalized) > BigDecimal(amount)
282
+
283
+ normalized
284
+ end
285
+
286
+ # UPI amounts are decimal with at most two places. Emitting anything else --
287
+ # `am=0`, `am=1499.5`, or the `0.30000000000000004` a float sum produces --
288
+ # is rejected by PSP apps, so normalize here and reject what cannot be
289
+ # represented rather than silently rounding someone's money.
290
+ def normalize_amount(amount, label)
291
+ return nil if amount.nil?
292
+ return nil if amount.is_a?(String) && amount.strip.empty?
293
+
294
+ decimal = to_decimal(amount, label)
295
+ raise ValidationError, "#{label} must be greater than zero, got #{amount.inspect}" unless decimal.positive?
296
+ raise ValidationError, "#{label} #{amount.inspect} is too large" if decimal > MAX_AMOUNT
297
+
298
+ if decimal != decimal.round(2)
299
+ raise ValidationError,
300
+ "#{label} #{amount.inspect} has more than two decimal places; " \
301
+ 'UPI amounts allow at most two. Round it before passing it in.'
302
+ end
303
+
304
+ whole, fraction = decimal.to_s('F').split('.')
305
+ "#{whole}.#{fraction.to_s.ljust(2, "0")[0, 2]}"
306
+ end
307
+
308
+ def to_decimal(amount, label)
309
+ case amount
310
+ when BigDecimal then amount
311
+ when Integer then BigDecimal(amount)
312
+ # 15 significant digits discards IEEE representation noise (0.1 + 0.2)
313
+ # while preserving precision the caller actually meant.
314
+ when Float, Rational then BigDecimal(amount, 15)
315
+ when String
316
+ value = amount.strip
317
+ raise ValidationError, "#{label} #{amount.inspect} is not a valid number" unless value.match?(NUMERIC_PATTERN)
318
+
319
+ BigDecimal(value)
89
320
  else
90
- raise ArgumentError, "Unsupported mode: #{mode}. Use :svg or :png."
321
+ raise ValidationError, "#{label} must be Numeric or String, got #{amount.class}"
91
322
  end
92
323
  end
93
324
 
94
- def upi_content(amount = '0', note = '', transaction_ref_id: nil, transaction_id: nil, url: nil)
95
- generate_transaction(amount, note, transaction_id, transaction_ref_id, url)
325
+ def presence(value)
326
+ value unless value.nil? || value.empty?
327
+ end
96
328
 
97
- # Manually construct the UPI URI string without URI::UPI
98
- query_string = if no_url_parse
99
- URI.encode_www_form(params.reject { |k, v| v.nil? || k == :pa })
100
- else
101
- params.reject { |k, v| v.nil? || k == :pa }.map { |k, v| "#{k}=#{v}" }.join('&')
102
- end
103
- "upi://pay?pa=#{params[:pa]}&#{query_string}"
329
+ def encode(value, lenient: false)
330
+ pattern = lenient || !no_url_parse ? LENIENT_UNSAFE : STRICT_UNSAFE
331
+ value.to_s.gsub(pattern) { |char| char.bytes.map { |byte| format('%%%02X', byte) }.join }
104
332
  end
105
333
 
106
- private
334
+ def validate_render_options!(options, mode)
335
+ allowed = mode == :svg ? SVG_OPTIONS : PNG_OPTIONS
336
+ unknown = options.keys - allowed
337
+ return if unknown.empty?
107
338
 
108
- def generate_transaction(amount, note, transaction_id, transaction_ref_id, url)
109
- @params[:am] = amount
110
- @params[:tn] = note
111
- @params[:tr] = transaction_ref_id
112
- @params[:tid] = transaction_id
113
- @params[:url] = url
114
- @params
339
+ raise ArgumentError,
340
+ "unknown keyword#{"s" if unknown.size > 1}: #{unknown.map(&:inspect).join(", ")}. " \
341
+ "Payment details go to #upi_content's parameters; #{mode} renderer options are: " \
342
+ "#{allowed.join(", ")}."
115
343
  end
116
- end
117
344
 
118
- class Error < StandardError; end
345
+ def render_svg(qrcode, options)
346
+ qrcode.as_svg({ color: '000', shape_rendering: 'crispEdges', module_size: 11 }.merge(options))
347
+ end
348
+
349
+ # Sizing is driven by module size rather than a fixed canvas: a longer
350
+ # payload needs more modules, and pinning the canvas shrinks each module
351
+ # until scanners start to struggle.
352
+ def render_png(qrcode, options)
353
+ defaults = {
354
+ bit_depth: 1,
355
+ border_modules: 4,
356
+ color_mode: ChunkyPNG::COLOR_GRAYSCALE,
357
+ color: 'black',
358
+ file: nil,
359
+ fill: 'white',
360
+ module_px_size: 8,
361
+ resize_exactly_to: false,
362
+ resize_gte_to: false
363
+ }
364
+ qrcode.as_png(**defaults.merge(options)).to_s
365
+ end
366
+ end
119
367
  end
@@ -1,16 +1,89 @@
1
1
  module Upi
2
+ class Error < StandardError
3
+ end
4
+
5
+ class ValidationError < Error
6
+ end
7
+
2
8
  class Generator
9
+ type amount = Numeric | String | nil
10
+ type fields = Hash[Symbol, String?]
11
+
12
+ CURRENCY: String
13
+ MODE_DEFAULT: String
14
+ MODE_QR: String
15
+ MODE_SECURE_QR: String
16
+ MODE_INTENT: String
17
+ MAX_LENGTHS: Hash[Symbol, Integer]
18
+ VPA_PATTERN: Regexp
19
+ MERCHANT_CODE_PATTERN: Regexp
20
+ REFERENCE_PATTERN: Regexp
21
+ NUMERIC_PATTERN: Regexp
22
+ MAX_AMOUNT: BigDecimal
23
+ STRICT_UNSAFE: Regexp
24
+ LENIENT_UNSAFE: Regexp
25
+ PARAM_ORDER: Array[Symbol]
26
+
27
+ @upi_id: String
28
+ @name: String
29
+ @currency: String
30
+ @merchant_code: String?
3
31
  @no_url_parse: bool
4
32
  @params: Hash[Symbol, String]
5
33
 
6
34
  attr_reader params: Hash[Symbol, String]
35
+ attr_reader no_url_parse: bool
36
+
37
+ def self.generate_reference: (?String prefix) -> String
38
+
39
+ def initialize: (
40
+ upi_id: String,
41
+ name: String,
42
+ ?currency: String,
43
+ ?merchant_code: String?,
44
+ ?no_url_parse: bool
45
+ ) -> void
7
46
 
8
- def generate_qr: -> String
47
+ def upi_content: (
48
+ ?amount amount,
49
+ ?String? note,
50
+ ?transaction_ref_id: String?,
51
+ ?transaction_id: String?,
52
+ ?url: String?,
53
+ ?min_amount: amount,
54
+ ?initiation_mode: String?
55
+ ) -> String
9
56
 
10
- def upi_content: -> String
57
+ def generate_qr: (
58
+ ?amount amount,
59
+ ?String? note,
60
+ ?transaction_ref_id: String?,
61
+ ?transaction_id: String?,
62
+ ?url: String?,
63
+ ?min_amount: amount,
64
+ ?initiation_mode: String?,
65
+ ?mode: Symbol,
66
+ ?level: Symbol,
67
+ **untyped render_options
68
+ ) -> String
11
69
 
12
70
  private
13
71
 
14
- def generate_transaction: -> Hash[Symbol, String]
72
+ def build_fields: (amount, String?, String?, String?, String?, amount, String?) -> fields
73
+ def require_reference!: (String?, String?) -> void
74
+ def validate_vpa!: (String) -> String
75
+ def validate_text!: (String?, Symbol, String) -> String?
76
+ def validate_currency!: (String) -> String
77
+ def validate_merchant_code!: (String?) -> String?
78
+ def validate_reference!: (String?, Symbol, String) -> String?
79
+ def validate_url!: (String?) -> String?
80
+ def validate_initiation_mode!: (String?) -> String?
81
+ def normalize_min_amount: (amount, String?) -> String?
82
+ def normalize_amount: (amount, String) -> String?
83
+ def to_decimal: (untyped, String) -> BigDecimal
84
+ def presence: (String?) -> String?
85
+ def encode: (untyped, ?lenient: bool) -> String
86
+ def render_svg: (untyped, Hash[Symbol, untyped]) -> String
87
+ def render_png: (untyped, Hash[Symbol, untyped]) -> String
15
88
  end
16
89
  end
data/upi.gemspec ADDED
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/upi/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'upi'
7
+ spec.version = Upi::VERSION
8
+ spec.authors = ['Faraz Noor']
9
+ spec.email = ['faraznoor75@gmail.com']
10
+
11
+ spec.summary = 'Generate UPI codes for payments.'
12
+ spec.description = 'This gem generates UPI QR codes for payments. It can be used in e-commerce applications to generate QR codes for payments. The QR codes can be scanned by UPI apps to make payments. The gem uses the rqrcode gem to generate the QR codes.'
13
+ spec.homepage = 'https://github.com/stingrayzboy/upi'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = '>= 2.6.0'
16
+
17
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
18
+
19
+ spec.metadata['homepage_uri'] = 'https://github.com/stingrayzboy/upi'
20
+ spec.metadata['source_code_uri'] = 'https://github.com/stingrayzboy/upi'
21
+ spec.metadata['changelog_uri'] = 'https://github.com/stingrayzboy/upi/blob/master/CHANGELOG.md'
22
+
23
+ # Specify which files should be added to the gem when it is released.
24
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
26
+ `git ls-files -z`.split("\x0").reject do |f|
27
+ (f == __FILE__) || f.match(%r{\A(?:(?:test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
28
+ end
29
+ end
30
+ spec.bindir = 'exe'
31
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
32
+ spec.require_paths = ['lib']
33
+
34
+ # Uncomment to register a new dependency of your gem
35
+ # spec.add_dependency "example-gem", "~> 1.0"
36
+ spec.add_runtime_dependency 'chunky_png'
37
+ spec.add_runtime_dependency 'rqrcode', '2.2.0'
38
+ # For more information and examples about making a new gem, check out our
39
+ # guide at: https://bundler.io/guides/creating_gem.html
40
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: upi
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.2
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Faraz Noor
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-09-08 00:00:00.000000000 Z
11
+ date: 2026-09-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: chunky_png
@@ -67,6 +67,7 @@ files:
67
67
  - lib/upi/version.rb
68
68
  - sig/upi.rbs
69
69
  - sig/upi/generator.rbs
70
+ - upi.gemspec
70
71
  homepage: https://github.com/stingrayzboy/upi
71
72
  licenses:
72
73
  - MIT
@@ -90,7 +91,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
90
91
  - !ruby/object:Gem::Version
91
92
  version: '0'
92
93
  requirements: []
93
- rubygems_version: 3.3.3
94
+ rubygems_version: 3.1.6
94
95
  signing_key:
95
96
  specification_version: 4
96
97
  summary: Generate UPI codes for payments.