payu-ruby 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 94b40d4686b64845659c12c92c956abb4d5d68e864ffe77875c2cc695e9a87d6
4
- data.tar.gz: 96af6c931aa710aac02c0bd4b0a16e60638db52bd1e58e535c77ed039df2e675
3
+ metadata.gz: 04a6b46c02485afa21bf0a5cb2a9f332b5dc46b726dca077b3be8940b5d5acec
4
+ data.tar.gz: 44048d1e821d50f7878e5398376204f11824ea9a0407d4691d3145f69c2ebcef
5
5
  SHA512:
6
- metadata.gz: d4f0fbbb6051bde54a99567d09e47c5749392854e6d9f8c1ae4645b85a8de14bc47a332a04eda68b06c4346413ba96ceab8da90bf4176f981191919f2cfcc2b8
7
- data.tar.gz: 5f237b35f525398bc77a74200e45172ae549cfed9cf589fedbc7fbc43adc42f3ff6ac31754e5a452a4608225f5690dcac2e277a81a4d22b572881ab39e1b660b
6
+ metadata.gz: a0aea058df01c76b2fac0afa23c3a9535ff3d4af73d8ff1f21f951ef3a7b936e9b576efa20dfb6868e3e704abdf5507f01d19281867d95d44e2958971408119c
7
+ data.tar.gz: 1c565df8f4ada5d8997f6cde3bc5c738bcede79939cb2c66c7ab203a0a6e6f4efb6cb2e77a6440578cf19de8d9d63955bf6b93d3fb24b7710783e55b14123510
@@ -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
- valid = Payu::Hash.valid_response?(params: params, salt: @config.salt)
58
- CallbackResult.new(params: params, valid: valid)
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
- # Response hash field order is REVERSED vs request (confirmed from docs.payu.in):
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
- # Variant: when PayU adds additional_charges (platform/convenience fee), it is prepended:
23
- # sha512(additional_charges|SALT|status|...|key)
24
- def self.response(params:, salt:)
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
- core = [
40
+ base_str = [
27
41
  salt,
28
- p[:status].to_s,
29
- "", "", "", "", "", # reserved slots (reversed)
30
- p[:udf5].to_s, p[:udf4].to_s, p[:udf3].to_s, p[:udf2].to_s, p[:udf1].to_s,
31
- p[:email].to_s, p[:firstname].to_s, p[:productinfo].to_s,
32
- p[:amount].to_s, p[:txnid].to_s, p[:key].to_s
33
- ]
34
- additional = p[:additional_charges].to_s
35
- parts = additional.empty? ? core : [additional] + core
36
- Digest::SHA512.hexdigest(parts.join("|"))
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
- expected = response(params: params, salt: salt)
42
- received = params[:hash].to_s
43
- return false if expected.bytesize != received.bytesize
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
- expected.bytes.zip(received.bytes).reduce(0) { |acc, (a, b)| acc | (a ^ b) }.zero?
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/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Payu
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  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.2.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.3
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: []