payu-ruby 0.1.0 → 0.3.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 +4 -4
- data/README.md +27 -3
- data/lib/payu/callback_result.rb +7 -2
- data/lib/payu/client.rb +4 -2
- data/lib/payu/hash.rb +58 -19
- data/lib/payu/payment_form.rb +37 -0
- data/lib/payu/version.rb +1 -1
- data/lib/payu.rb +4 -3
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 04a6b46c02485afa21bf0a5cb2a9f332b5dc46b726dca077b3be8940b5d5acec
|
|
4
|
+
data.tar.gz: 44048d1e821d50f7878e5398376204f11824ea9a0407d4691d3145f69c2ebcef
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a0aea058df01c76b2fac0afa23c3a9535ff3d4af73d8ff1f21f951ef3a7b936e9b576efa20dfb6868e3e704abdf5507f01d19281867d95d44e2958971408119c
|
|
7
|
+
data.tar.gz: 1c565df8f4ada5d8997f6cde3bc5c738bcede79939cb2c66c7ab203a0a6e6f4efb6cb2e77a6440578cf19de8d9d63955bf6b93d3fb24b7710783e55b14123510
|
data/README.md
CHANGED
|
@@ -57,9 +57,33 @@ refund.status(result.mihpayid)
|
|
|
57
57
|
reported status is informational only; always follow up with
|
|
58
58
|
`verify_payment` before treating a transaction as successful.
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
## Auto-submit checkout page (web + mobile)
|
|
61
|
+
|
|
62
|
+
`form.to_html` renders a complete, self-contained HTML page that
|
|
63
|
+
auto-submits to PayU on load — a hidden form with one input per field,
|
|
64
|
+
`onload="document.forms[0].submit()"`, and a `<noscript>` fallback button.
|
|
65
|
+
Values are HTML-escaped.
|
|
66
|
+
|
|
67
|
+
Expose one route that renders it:
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
# GET /checkout/:id/pay
|
|
71
|
+
def pay
|
|
72
|
+
form = client.build_payment(...)
|
|
73
|
+
render html: form.to_html.html_safe, content_type: "text/html"
|
|
74
|
+
end
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
That single public URL (e.g. `https://app.example.com/checkout/123/pay`) is
|
|
78
|
+
all any client needs — no client-side form building required either way:
|
|
79
|
+
|
|
80
|
+
- **Web** — redirect the browser to it, or link to it.
|
|
81
|
+
- **Mobile** — point a WebView at the same URL; the page auto-submits to
|
|
82
|
+
PayU itself, so the app never needs to construct the POST or handle
|
|
83
|
+
`fields` directly.
|
|
84
|
+
|
|
85
|
+
`form.to_h` (`{ payment_url:, fields: }`) is also available if you'd rather
|
|
86
|
+
hand the raw data to your own frontend code instead of using `to_html`.
|
|
63
87
|
|
|
64
88
|
## Error handling
|
|
65
89
|
|
data/lib/payu/callback_result.rb
CHANGED
|
@@ -5,11 +5,12 @@ module Payu
|
|
|
5
5
|
# This is informational only: PayU's reported status here must not be
|
|
6
6
|
# trusted for final state. Always follow up with Client#verify_payment.
|
|
7
7
|
class CallbackResult
|
|
8
|
-
attr_reader :params, :valid
|
|
8
|
+
attr_reader :params, :valid, :matched_hash_form
|
|
9
9
|
|
|
10
|
-
def initialize(params:, valid:)
|
|
10
|
+
def initialize(params:, valid:, matched_hash_form: nil)
|
|
11
11
|
@params = params
|
|
12
12
|
@valid = valid
|
|
13
|
+
@matched_hash_form = matched_hash_form
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
def valid? = @valid
|
|
@@ -27,5 +28,9 @@ module Payu
|
|
|
27
28
|
def udf3 = @params[:udf3]
|
|
28
29
|
def udf4 = @params[:udf4]
|
|
29
30
|
def udf5 = @params[:udf5]
|
|
31
|
+
|
|
32
|
+
def additional_charges
|
|
33
|
+
Payu::Hash.extract_additional_charges(@params)
|
|
34
|
+
end
|
|
30
35
|
end
|
|
31
36
|
end
|
data/lib/payu/client.rb
CHANGED
|
@@ -54,8 +54,10 @@ module Payu
|
|
|
54
54
|
# PayU's reported status here is informational only — never trust it
|
|
55
55
|
# for final state; always follow up with #verify_payment.
|
|
56
56
|
def verify_callback(params)
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
result = Payu::Hash.valid_response?(params: params, salt: @config.salt)
|
|
58
|
+
valid = result != false
|
|
59
|
+
matched_form = valid ? result : nil
|
|
60
|
+
CallbackResult.new(params: params, valid: valid, matched_hash_form: matched_form)
|
|
59
61
|
end
|
|
60
62
|
|
|
61
63
|
# Server-to-server reconciliation — the source of truth for transaction
|
data/lib/payu/hash.rb
CHANGED
|
@@ -16,33 +16,72 @@ module Payu
|
|
|
16
16
|
Digest::SHA512.hexdigest(parts.join("|"))
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
#
|
|
19
|
+
# Extract additionalCharges regardless of key style (camelCase or snake_case,
|
|
20
|
+
# string or symbol). PayU sends camelCase in callbacks.
|
|
21
|
+
def self.extract_additional_charges(params)
|
|
22
|
+
val = params[:additionalCharges] || params["additionalCharges"] ||
|
|
23
|
+
params[:additional_charges] || params["additional_charges"]
|
|
24
|
+
v = val.to_s
|
|
25
|
+
v.empty? ? nil : v
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Response hash candidates — returns a hash of candidate name => SHA512 digest.
|
|
29
|
+
#
|
|
30
|
+
# When additionalCharges is present, PayU may sign with either the plain or
|
|
31
|
+
# prefixed form. Both candidates are returned so the caller can try each.
|
|
32
|
+
#
|
|
33
|
+
# Reverse hash formula (plain):
|
|
20
34
|
# sha512(SALT|status||||||udf5|udf4|udf3|udf2|udf1|email|firstname|productinfo|amount|txnid|key)
|
|
21
35
|
#
|
|
22
|
-
#
|
|
23
|
-
# sha512(
|
|
24
|
-
def self.
|
|
36
|
+
# Reverse hash formula (with additional charges):
|
|
37
|
+
# sha512(additionalCharges|SALT|status||||||udf5|udf4|udf3|udf2|udf1|email|firstname|productinfo|amount|txnid|key)
|
|
38
|
+
def self.response_candidates(params:, salt:)
|
|
25
39
|
p = params
|
|
26
|
-
|
|
40
|
+
base_str = [
|
|
27
41
|
salt,
|
|
28
|
-
p[:status].to_s,
|
|
29
|
-
"", "", "", "", "",
|
|
30
|
-
p[:udf5]
|
|
31
|
-
p[:
|
|
32
|
-
p[:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
42
|
+
(p[:status] || p["status"]).to_s,
|
|
43
|
+
"", "", "", "", "",
|
|
44
|
+
(p[:udf5] || p["udf5"]).to_s,
|
|
45
|
+
(p[:udf4] || p["udf4"]).to_s,
|
|
46
|
+
(p[:udf3] || p["udf3"]).to_s,
|
|
47
|
+
(p[:udf2] || p["udf2"]).to_s,
|
|
48
|
+
(p[:udf1] || p["udf1"]).to_s,
|
|
49
|
+
(p[:email] || p["email"]).to_s,
|
|
50
|
+
(p[:firstname] || p["firstname"]).to_s,
|
|
51
|
+
(p[:productinfo] || p["productinfo"]).to_s,
|
|
52
|
+
(p[:amount] || p["amount"]).to_s,
|
|
53
|
+
(p[:txnid] || p["txnid"]).to_s,
|
|
54
|
+
(p[:key] || p["key"]).to_s
|
|
55
|
+
].join("|")
|
|
56
|
+
|
|
57
|
+
charges = extract_additional_charges(params)
|
|
58
|
+
candidates = { "plain" => Digest::SHA512.hexdigest(base_str) }
|
|
59
|
+
candidates["additional_charges"] = Digest::SHA512.hexdigest("#{charges}|#{base_str}") if charges
|
|
60
|
+
candidates
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Response hash — returns the expected reverse hash for verification.
|
|
64
|
+
# When additionalCharges is present, returns the prefixed hash; otherwise plain.
|
|
65
|
+
def self.response(params:, salt:)
|
|
66
|
+
charges = extract_additional_charges(params)
|
|
67
|
+
candidates = response_candidates(params: params, salt: salt)
|
|
68
|
+
charges ? candidates["additional_charges"] : candidates["plain"]
|
|
37
69
|
end
|
|
38
70
|
|
|
39
|
-
# Constant-time comparison to prevent timing attacks
|
|
71
|
+
# Constant-time comparison to prevent timing attacks.
|
|
72
|
+
# Returns the matched form name ("plain" or "additional_charges") on success,
|
|
73
|
+
# or false on failure. Truthy/falsy contract is preserved for backward compat.
|
|
40
74
|
def self.valid_response?(params:, salt:)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
75
|
+
received = (params[:hash] || params["hash"]).to_s
|
|
76
|
+
return false if received.empty?
|
|
77
|
+
|
|
78
|
+
candidates = response_candidates(params: params, salt: salt)
|
|
79
|
+
matched = candidates.find do |_name, expected|
|
|
80
|
+
next false if expected.bytesize != received.bytesize
|
|
81
|
+
expected.bytes.zip(received.bytes).reduce(0) { |acc, (a, b)| acc | (a ^ b) }.zero?
|
|
82
|
+
end
|
|
44
83
|
|
|
45
|
-
|
|
84
|
+
matched ? matched.first : false
|
|
46
85
|
end
|
|
47
86
|
|
|
48
87
|
# Hash for server-to-server API commands: sha512(key|command|var1|salt)
|
data/lib/payu/payment_form.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require "cgi"
|
|
2
|
+
|
|
1
3
|
module Payu
|
|
2
4
|
# Return value of Client#build_payment. No network call is made to
|
|
3
5
|
# produce this — it's just the signed field set and the URL to post it to.
|
|
@@ -8,5 +10,40 @@ module Payu
|
|
|
8
10
|
@payment_url = payment_url
|
|
9
11
|
@fields = fields
|
|
10
12
|
end
|
|
13
|
+
|
|
14
|
+
# Shape expected by the bundled payu-checkout.js: new PayuCheckout(data).open()
|
|
15
|
+
def to_h = { payment_url: @payment_url, fields: @fields }
|
|
16
|
+
|
|
17
|
+
# A complete, self-contained HTML page that auto-submits to PayU on
|
|
18
|
+
# load. Render this from a single GET route (e.g. GET /checkout/:id/pay)
|
|
19
|
+
# and that URL is all a client needs: redirect a browser to it, or point
|
|
20
|
+
# a mobile WebView at it — no client-side form building required either
|
|
21
|
+
# way. Values are HTML-escaped; a <noscript> button covers JS-disabled
|
|
22
|
+
# browsers.
|
|
23
|
+
def to_html
|
|
24
|
+
inputs = @fields.map { |name, value| hidden_input(name, value) }.join("\n ")
|
|
25
|
+
|
|
26
|
+
<<~HTML
|
|
27
|
+
<!DOCTYPE html>
|
|
28
|
+
<html>
|
|
29
|
+
<head>
|
|
30
|
+
<meta charset="utf-8">
|
|
31
|
+
<title>Redirecting to PayU…</title>
|
|
32
|
+
</head>
|
|
33
|
+
<body onload="document.forms[0].submit()">
|
|
34
|
+
<form method="POST" action="#{CGI.escapeHTML(@payment_url)}">
|
|
35
|
+
#{inputs}
|
|
36
|
+
<noscript><button type="submit">Continue to payment</button></noscript>
|
|
37
|
+
</form>
|
|
38
|
+
</body>
|
|
39
|
+
</html>
|
|
40
|
+
HTML
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def hidden_input(name, value)
|
|
46
|
+
%(<input type="hidden" name="#{CGI.escapeHTML(name.to_s)}" value="#{CGI.escapeHTML(value.to_s)}">)
|
|
47
|
+
end
|
|
11
48
|
end
|
|
12
49
|
end
|
data/lib/payu/version.rb
CHANGED
data/lib/payu.rb
CHANGED
|
@@ -32,6 +32,7 @@ require "payu/refund"
|
|
|
32
32
|
# )
|
|
33
33
|
# form.payment_url # => URL to POST form.fields to (form submit or WebView)
|
|
34
34
|
# form.fields # => Hash of all form fields including :hash
|
|
35
|
+
# form.to_html # => self-contained auto-submitting checkout page
|
|
35
36
|
#
|
|
36
37
|
# # 2. Verify an inbound callback/redirect's hash (never trust it for final state)
|
|
37
38
|
# callback = client.verify_callback(params)
|
|
@@ -43,8 +44,8 @@ require "payu/refund"
|
|
|
43
44
|
# result.mihpayid
|
|
44
45
|
# result.raw # full parsed JSON, for audit logging
|
|
45
46
|
#
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
#
|
|
47
|
+
# form.to_html renders a complete auto-submitting checkout page — expose it
|
|
48
|
+
# from one GET route and that URL is all any client needs: redirect a
|
|
49
|
+
# browser to it, or point a mobile WebView at it. See README for details.
|
|
49
50
|
module Payu
|
|
50
51
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: payu-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vishwajeetsingh Desurkar
|
|
@@ -77,7 +77,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
77
77
|
- !ruby/object:Gem::Version
|
|
78
78
|
version: '0'
|
|
79
79
|
requirements: []
|
|
80
|
-
rubygems_version: 4.0.
|
|
80
|
+
rubygems_version: 4.0.6
|
|
81
81
|
specification_version: 4
|
|
82
82
|
summary: Ruby client for PayU India payment gateway
|
|
83
83
|
test_files: []
|