upi 2.0.1 → 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: fbc938b77f542923739627acea2a7b852324d4e1facc7abc9e110fb57d6a468f
4
- data.tar.gz: cb6e4d8010da2c1cec18469c08edef3a4349807033fd68d04a6d209042443a64
3
+ metadata.gz: 0c35150decd0ea8da417f11c7d5513cdf82a3609a61b071d9e41d6977ec0b802
4
+ data.tar.gz: d24b6754719ce81625612e38827d8a453df4d9a09b1a7d6966ff71c6972f0289
5
5
  SHA512:
6
- metadata.gz: 10bd3fdca2b006501ae4166c96443217ddd3bf72dc8c12d5474aeee346e5b56f48339bd413fca25c33aada355069b53021337930b33bd04b5de1e41007d9b5fa
7
- data.tar.gz: 17475c5118a332063e18d4547e021dccce597922c589f7e9c819e494b869881e2eaea5ec82b54a364ccfd4b729069e07e02e1593f114ba712f1da2cfbf872394
6
+ metadata.gz: 5eba315c7f151124b1087ceba8555358fa5b95e3f07bc208e314b7f82f7497ea9617a9cab45bc7599a29574ef739b8e38a9067a68c3f10b9cf5aab100d9271f3
7
+ data.tar.gz: 7fe580edf651ff6eb855c66996fb7013121800a96ef5d8c8f6dc2a3690c9a14847d7cdf403ed3b78b6b59f59a005076de5b2e4b0d52c445486754e2db083f3c6
data/.idea/upi.iml CHANGED
@@ -13,6 +13,7 @@
13
13
  <orderEntry type="sourceFolder" forTests="false" />
14
14
  <orderEntry type="library" scope="PROVIDED" name="ast (v2.4.2, RVM: ruby-3.1.0) [gem]" level="application" />
15
15
  <orderEntry type="library" scope="PROVIDED" name="bundler (v2.3.3, RVM: ruby-3.1.0) [gem]" level="application" />
16
+ <orderEntry type="library" scope="PROVIDED" name="byebug (v11.1.3, RVM: ruby-3.1.0) [gem]" level="application" />
16
17
  <orderEntry type="library" scope="PROVIDED" name="chunky_png (v1.4.0, RVM: ruby-3.1.0) [gem]" level="application" />
17
18
  <orderEntry type="library" scope="PROVIDED" name="diff-lcs (v1.5.1, RVM: ruby-3.1.0) [gem]" level="application" />
18
19
  <orderEntry type="library" scope="PROVIDED" name="json (v2.7.2, RVM: ruby-3.1.0) [gem]" level="application" />
@@ -39,21 +40,21 @@
39
40
  <option name="myRootTask">
40
41
  <RakeTaskImpl id="rake">
41
42
  <subtasks>
42
- <RakeTaskImpl description="Build upi-2.0.0.gem into the pkg directory" fullCommand="build" id="build" />
43
+ <RakeTaskImpl description="Build upi-2.0.2.gem into the pkg directory" fullCommand="build" id="build" />
43
44
  <RakeTaskImpl id="build">
44
45
  <subtasks>
45
- <RakeTaskImpl description="Generate SHA512 checksum if upi-2.0.0.gem into the checksums directory" fullCommand="build:checksum" id="checksum" />
46
+ <RakeTaskImpl description="Generate SHA512 checksum if upi-2.0.2.gem into the checksums directory" fullCommand="build:checksum" id="checksum" />
46
47
  </subtasks>
47
48
  </RakeTaskImpl>
48
49
  <RakeTaskImpl description="Remove any temporary products" fullCommand="clean" id="clean" />
49
50
  <RakeTaskImpl description="Remove any generated files" fullCommand="clobber" id="clobber" />
50
- <RakeTaskImpl description="Build and install upi-2.0.0.gem into system gems" fullCommand="install" id="install" />
51
+ <RakeTaskImpl description="Build and install upi-2.0.2.gem into system gems" fullCommand="install" id="install" />
51
52
  <RakeTaskImpl id="install">
52
53
  <subtasks>
53
- <RakeTaskImpl description="Build and install upi-2.0.0.gem into system gems without network access" fullCommand="install:local" id="local" />
54
+ <RakeTaskImpl description="Build and install upi-2.0.2.gem into system gems without network access" fullCommand="install:local" id="local" />
54
55
  </subtasks>
55
56
  </RakeTaskImpl>
56
- <RakeTaskImpl description="Create tag v2.0.0 and build and push upi-2.0.0.gem to https://rubygems.org" fullCommand="release[remote]" id="release[remote]" />
57
+ <RakeTaskImpl description="Create tag v2.0.2 and build and push upi-2.0.2.gem to https://rubygems.org" fullCommand="release[remote]" id="release[remote]" />
57
58
  <RakeTaskImpl description="Run RuboCop" fullCommand="rubocop" id="rubocop" />
58
59
  <RakeTaskImpl id="rubocop">
59
60
  <subtasks>
data/.rubocop.yml CHANGED
@@ -15,8 +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
20
+
21
+ Metrics/AbcSize:
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,4 +1,61 @@
1
1
  ## [Unreleased]
2
+ - In the making
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
+
52
+ ## [2.0.2] - 2024-09-08
53
+ - For Individual Mode there need not be merchant code being sent. Removed default '0000'. Can be still added explicitly.
54
+ - Changed the upi_content method to not parse the upi address and keep it as it is.
55
+ - Add support for no uri parse.
56
+
57
+ ## [2.0.1] - 2024-09-06
58
+ - Updated Dependencies
2
59
 
3
60
  ## [2.0.0] - 2024-09-06
4
61
  - Add support for changing amounts after initialization
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- upi (2.0.1)
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,105 +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
+ ```
75
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:
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'
89
+ )
90
+ ```
91
+
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:
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
81
106
  )
107
+ ```
108
+
109
+ In practice the reference is your own order or invoice number, which is what makes
110
+ reconciliation work:
111
+
112
+ ```ruby
113
+ generator.upi_content(500, 'Payment for Goods', transaction_ref_id: "ORD#{order.id}")
114
+ ```
82
115
 
83
- # NOTE: For Merchant Mode the Merchant Code needs to be set explicitly
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:
84
118
 
119
+ ```ruby
120
+ generator.upi_content
121
+ # => "upi://pay?pa=merchant@upi&pn=Test%20Name&mc=5411&cu=INR"
85
122
  ```
86
123
 
87
- ### Generate QR code in SVG format
124
+ ### Letting the payer pay at least a minimum
125
+
126
+ `min_amount` maps to the `mam` tag: the amount becomes editable, down to that floor.
127
+
88
128
  ```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)
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"
91
131
  ```
92
132
 
93
- ### Generate QR code in PNG format
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
+
94
165
  ```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)
166
+ generator.generate_qr(100, 'Tea', mode: :png, module_px_size: 12, color: '333')
97
167
  ```
98
168
 
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.
169
+ ## Amounts
101
170
 
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.
102
173
 
103
174
  ```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
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'))
107
179
 
108
- # The generate_url method returns a UPI URI string that can be used as a link in your HTML:
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
109
182
  ```
110
183
 
111
- ```html
112
- <a href="<%= payment_url %>">Pay Now</a>
184
+ Pass a `String` or `BigDecimal` when the amount comes from a currency column, so no float ever
185
+ touches it.
186
+
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.
193
+
194
+ ```ruby
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
+ ```
198
+
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"
113
221
  ```
114
222
 
115
- #### In the example above:
223
+ Signed QR codes (`mode=02`, requiring the `sign` and `orgid` tags) are not supported.
116
224
 
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.
225
+ ## Upgrading from 2.x
131
226
 
132
- #### Handling PNG and SVG
133
- You can specify the format of the QR code by using the mode parameter:
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:
134
229
 
135
- * mode: :svg generates an SVG format QR code.
136
- * mode: :png generates a PNG format QR code.
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.
137
239
 
138
240
  ## Development
139
241
 
@@ -143,7 +245,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
143
245
 
144
246
  ## Contributing
145
247
 
146
- 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).
147
249
 
148
250
  ## License
149
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.1'
4
+ VERSION = '3.0.0'
5
5
  end
data/lib/upi.rb CHANGED
@@ -2,113 +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
46
- attr_reader :params
47
-
48
- def initialize(upi_id:, name:, currency: 'INR', merchant_code: '0000')
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
60
- end
61
-
62
- def generate_qr(amount = '0', note = '', transaction_ref_id: nil, transaction_id: nil, url: nil, mode: :svg)
63
- content = upi_content(amount, note, transaction_ref_id: transaction_ref_id, transaction_id: transaction_id, url: url)
64
- qrcode = RQRCode::QRCode.new(content)
65
- generate_transaction(amount, note, transaction_id, transaction_ref_id, url)
66
-
67
- case mode
68
- when :svg
69
- qrcode.as_svg(
70
- color: '000',
71
- shape_rendering: 'crispEdges',
72
- module_size: 11
73
- )
74
- when :png
75
- png = qrcode.as_png(
76
- bit_depth: 1,
77
- border_modules: 4,
78
- color_mode: ChunkyPNG::COLOR_GRAYSCALE,
79
- color: 'black',
80
- file: nil,
81
- fill: 'white',
82
- module_px_size: 6, # Adjust size as needed
83
- resize_exactly_to: false,
84
- resize_gte_to: false,
85
- size: 300 # Adjust size as needed
86
- )
87
- png.to_s
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
+
90
+ attr_reader :params, :no_url_parse
91
+
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)
106
+ @no_url_parse = no_url_parse
107
+ @params = { pa: @upi_id, pn: @name, mc: @merchant_code, cu: @currency }.compact.freeze
108
+ end
109
+
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)
88
320
  else
89
- raise ArgumentError, "Unsupported mode: #{mode}. Use :svg or :png."
321
+ raise ValidationError, "#{label} must be Numeric or String, got #{amount.class}"
90
322
  end
91
323
  end
92
324
 
93
- def upi_content(amount = '0', note = '', transaction_ref_id: nil, transaction_id: nil, url: nil)
94
- generate_transaction(amount, note, transaction_id, transaction_ref_id, url)
325
+ def presence(value)
326
+ value unless value.nil? || value.empty?
327
+ end
95
328
 
96
- # Manually construct the UPI URI string without URI::UPI
97
- query_string = URI.encode_www_form(params.reject { |_k, v| v.nil? })
98
- "upi://pay?#{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 }
99
332
  end
100
333
 
101
- 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?
102
338
 
103
- def generate_transaction(amount, note, transaction_id, transaction_ref_id, url)
104
- @params[:am] = amount
105
- @params[:tn] = note
106
- @params[:tr] = transaction_ref_id
107
- @params[:tid] = transaction_id
108
- @params[:url] = url
109
- @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(", ")}."
110
343
  end
111
- end
112
344
 
113
- 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
114
367
  end
@@ -1,15 +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?
31
+ @no_url_parse: bool
3
32
  @params: Hash[Symbol, String]
4
33
 
5
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
6
46
 
7
- 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
8
56
 
9
- 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
10
69
 
11
70
  private
12
71
 
13
- 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
14
88
  end
15
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.1
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-06 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.