paygate_pk 0.1.0 → 0.2.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/CHANGELOG.md +6 -0
- data/Gemfile.lock +6 -1
- data/README.md +24 -24
- data/lib/paygate_pk/config.rb +3 -1
- data/lib/paygate_pk/contracts/bearer_token.rb +18 -0
- data/lib/paygate_pk/contracts/instrument.rb +10 -0
- data/lib/paygate_pk/contracts/webhook_event.rb +22 -0
- data/lib/paygate_pk/providers/pay_fast/auth.rb +4 -0
- data/lib/paygate_pk/providers/pay_fast/checkout.rb +4 -0
- data/lib/paygate_pk/providers/pay_fast/client.rb +21 -1
- data/lib/paygate_pk/providers/pay_fast/tokenization/instrument.rb +63 -0
- data/lib/paygate_pk/providers/pay_fast/tokenization/token.rb +65 -0
- data/lib/paygate_pk/providers/pay_fast/webhook.rb +72 -0
- data/lib/paygate_pk/util/security.rb +27 -0
- data/lib/paygate_pk/util/signature.rb +18 -0
- data/lib/paygate_pk/version.rb +1 -1
- data/lib/paygate_pk.rb +6 -0
- data/paygate_pk.gemspec +2 -2
- metadata +38 -16
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5e12304e544277a26c17f5ce400fc0e43ea411fccd80976dc44f5262540f2fd2
|
|
4
|
+
data.tar.gz: 8eb47d711ec0dad930b961006168528b29d37aa076011f835781e79f6890840c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5c32927ed495619815a9760f12c23e40f327d8122f079f66c28a45ed6fc9448a8d3b4344fbbec9f99c51888155dd42537272b1c1b629578f40c5e541f0cbdf76
|
|
7
|
+
data.tar.gz: 5b85b0f2ccd4c03a703480cf3dfa1ee3c207f97f708be8e66b6a7e90eefbb5e12c3e13bb3ad39ccca638345e2a0c72022c1e234b9f7a09e6ef43dc5fec675414
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.0] - 2025-10-10
|
|
4
|
+
|
|
5
|
+
- Added: IPN verification, Tokenization 3.1 & 3.15.
|
|
6
|
+
- Changed: renamed token methods (old name deprecated).
|
|
7
|
+
- Security: switched to stdlib constant-time compare (no Rack dep).
|
|
8
|
+
|
|
3
9
|
## [0.1.0] - 2025-10-01
|
|
4
10
|
|
|
5
11
|
- Initial release.
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
paygate_pk (0.
|
|
4
|
+
paygate_pk (0.2.0)
|
|
5
5
|
faraday (>= 2.7)
|
|
6
6
|
faraday-retry (>= 2.0)
|
|
7
7
|
json
|
|
@@ -14,6 +14,7 @@ GEM
|
|
|
14
14
|
public_suffix (>= 2.0.2, < 7.0)
|
|
15
15
|
ast (2.4.3)
|
|
16
16
|
bigdecimal (3.2.3)
|
|
17
|
+
byebug (12.0.0)
|
|
17
18
|
crack (1.0.0)
|
|
18
19
|
bigdecimal
|
|
19
20
|
rexml
|
|
@@ -36,6 +37,8 @@ GEM
|
|
|
36
37
|
uri
|
|
37
38
|
nokogiri (1.18.9-x86_64-darwin)
|
|
38
39
|
racc (~> 1.4)
|
|
40
|
+
nokogiri (1.18.9-x86_64-linux-gnu)
|
|
41
|
+
racc (~> 1.4)
|
|
39
42
|
parallel (1.27.0)
|
|
40
43
|
parser (3.3.8.0)
|
|
41
44
|
ast (~> 2.4.1)
|
|
@@ -79,8 +82,10 @@ GEM
|
|
|
79
82
|
|
|
80
83
|
PLATFORMS
|
|
81
84
|
x86_64-darwin-24
|
|
85
|
+
x86_64-linux
|
|
82
86
|
|
|
83
87
|
DEPENDENCIES
|
|
88
|
+
byebug
|
|
84
89
|
minitest (~> 5.0)
|
|
85
90
|
paygate_pk!
|
|
86
91
|
rake (~> 13.0)
|
data/README.md
CHANGED
|
@@ -15,11 +15,11 @@ This gem provides a clean, provider-agnostic interface to obtain access tokens a
|
|
|
15
15
|
|
|
16
16
|
Install the gem and add to the application's Gemfile by executing:
|
|
17
17
|
|
|
18
|
-
$ bundle add "paygate_pk", "~> 0.
|
|
18
|
+
$ bundle add "paygate_pk", "~> 0.2.0"
|
|
19
19
|
|
|
20
20
|
If bundler is not being used to manage dependencies, install the gem by executing:
|
|
21
21
|
|
|
22
|
-
$ gem install "paygate_pk", "~> 0.
|
|
22
|
+
$ gem install "paygate_pk", "~> 0.2.0"
|
|
23
23
|
|
|
24
24
|
## Usage
|
|
25
25
|
|
|
@@ -34,10 +34,10 @@ PaygatePk.configure do |c|
|
|
|
34
34
|
|
|
35
35
|
# PayFast base host only; endpoints include /Ecommerce/api internally
|
|
36
36
|
|
|
37
|
-
c.
|
|
38
|
-
c.
|
|
39
|
-
c.
|
|
40
|
-
c.
|
|
37
|
+
c.pay_fast.base_url = "https://ipguat.apps.net.pk"
|
|
38
|
+
c.pay_fast.merchant_id = ENV.fetch("PAYFAST_MERCHANT_ID")
|
|
39
|
+
c.pay_fast.secured_key = ENV.fetch("PAYFAST_SECURED_KEY")
|
|
40
|
+
c.pay_fast.api_base_url = "https://api.getfrompayfast.com"
|
|
41
41
|
|
|
42
42
|
# Optional: tune timeouts & retries
|
|
43
43
|
|
|
@@ -51,7 +51,8 @@ end
|
|
|
51
51
|
# 1) Get Access Token (PayFast)
|
|
52
52
|
|
|
53
53
|
```ruby
|
|
54
|
-
|
|
54
|
+
|
|
55
|
+
client = PaygatePk::Providers::PayFast::Auth.new
|
|
55
56
|
|
|
56
57
|
token_obj = auth.get_access_token(
|
|
57
58
|
basket_id: "B-1001",
|
|
@@ -67,25 +68,24 @@ puts token_obj.token # => "ACCESS_TOKEN_STRING"
|
|
|
67
68
|
|
|
68
69
|
```
|
|
69
70
|
|
|
70
|
-
# 2)
|
|
71
|
+
# 2) Verify IPN
|
|
71
72
|
|
|
72
73
|
```ruby
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
#
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
# Redirect the buyer to hc.url
|
|
74
|
+
verified_data = client.verify_ipn!(request.params)
|
|
75
|
+
|
|
76
|
+
:provider, # Symbol e.g., :payfast
|
|
77
|
+
:transaction_id, # String or nil
|
|
78
|
+
:basket_id, # String
|
|
79
|
+
:order_date, # String (YYYY-MM-DD) or Time/Date if you coerce later
|
|
80
|
+
:approved, # Boolean (true if err_code == "000")
|
|
81
|
+
:code, # Provider code, e.g., "000"
|
|
82
|
+
:message, # Human-readable message
|
|
83
|
+
:amount, # String/Integer (as received)
|
|
84
|
+
:currency, # String "PKR" etc.
|
|
85
|
+
:instrument_token, # String or nil (for tokenized flows)
|
|
86
|
+
:recurring, # Boolean
|
|
87
|
+
:raw, # Original params Hash (unmodified input)
|
|
88
|
+
|
|
89
89
|
```
|
|
90
90
|
|
|
91
91
|
# Error handling
|
data/lib/paygate_pk/config.rb
CHANGED
|
@@ -28,7 +28,8 @@ module PaygatePk
|
|
|
28
28
|
|
|
29
29
|
# Provider-specific configuration
|
|
30
30
|
class ProviderConfig
|
|
31
|
-
attr_accessor :base_url, :merchant_id, :secured_key, :checkout_mode, :username, :password,
|
|
31
|
+
attr_accessor :api_base_url, :base_url, :merchant_id, :secured_key, :checkout_mode, :username, :password,
|
|
32
|
+
:store_id
|
|
32
33
|
|
|
33
34
|
def initialize
|
|
34
35
|
@base_url = nil
|
|
@@ -38,6 +39,7 @@ module PaygatePk
|
|
|
38
39
|
@username = nil
|
|
39
40
|
@password = nil
|
|
40
41
|
@store_id = nil
|
|
42
|
+
@api_base_url = nil
|
|
41
43
|
end
|
|
42
44
|
end
|
|
43
45
|
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# lib/paygate_pk/contracts/access_token.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module PaygatePk
|
|
5
|
+
module Contracts
|
|
6
|
+
# Normalized wrapper for /token response.
|
|
7
|
+
# Some docs show only "refresh_token"; we expose both.
|
|
8
|
+
BearerToken = Struct.new(
|
|
9
|
+
:access_token, # String or nil
|
|
10
|
+
:refresh_token, # String or nil
|
|
11
|
+
:expiry, # Integer seconds or nil
|
|
12
|
+
:code, # Provider code string or nil
|
|
13
|
+
:message, # Provider message string or nil
|
|
14
|
+
:raw, # Full response Hash
|
|
15
|
+
keyword_init: true
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PaygatePk
|
|
4
|
+
module Contracts
|
|
5
|
+
# Normalized server-side notification from PayFast (IPN) or other providers.
|
|
6
|
+
WebhookEvent = Struct.new(
|
|
7
|
+
:provider, # Symbol e.g., :payfast
|
|
8
|
+
:transaction_id, # String or nil
|
|
9
|
+
:basket_id, # String
|
|
10
|
+
:order_date, # String (YYYY-MM-DD) or Time/Date if you coerce later
|
|
11
|
+
:approved, # Boolean (true if err_code == "000")
|
|
12
|
+
:code, # Provider code, e.g., "000"
|
|
13
|
+
:message, # Human-readable message
|
|
14
|
+
:amount, # String/Integer (as received)
|
|
15
|
+
:currency, # String "PKR" etc.
|
|
16
|
+
:instrument_token, # String or nil (for tokenized flows)
|
|
17
|
+
:recurring, # Boolean
|
|
18
|
+
:raw, # Original params Hash (unmodified input)
|
|
19
|
+
keyword_init: true
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -12,6 +12,26 @@ module PaygatePk
|
|
|
12
12
|
@config = config
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
+
def get_access_token(**params)
|
|
16
|
+
Auth.new(config: @config).get_access_token(**params)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def verify_ipn!(params)
|
|
20
|
+
Webhook.new(config: @config).verify!(params)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def create_checkout(**params)
|
|
24
|
+
Checkout.new(config: @config).create!(**params)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def get_bearer_token(**params)
|
|
28
|
+
Tokenization::Token.new(config: @config).get(**params)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def instruments(**params)
|
|
32
|
+
Tokenization::Instrument.new(config: @config).list(**params)
|
|
33
|
+
end
|
|
34
|
+
|
|
15
35
|
private
|
|
16
36
|
|
|
17
37
|
attr_reader :config
|
|
@@ -20,7 +40,7 @@ module PaygatePk
|
|
|
20
40
|
raise ConfigurationError, "PayFast base_url not set" unless config.base_url
|
|
21
41
|
|
|
22
42
|
PaygatePk::HTTP::Client.new(
|
|
23
|
-
base_url:
|
|
43
|
+
base_url: base_url,
|
|
24
44
|
headers: { "Accept" => "application/json" },
|
|
25
45
|
timeouts: PaygatePk.config.timeouts,
|
|
26
46
|
retry_conf: PaygatePk.config.retry,
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PaygatePk
|
|
4
|
+
module Providers
|
|
5
|
+
module PayFast
|
|
6
|
+
module Tokenization
|
|
7
|
+
# used to generate the bearer_token
|
|
8
|
+
class Instrument < PaygatePk::Providers::PayFast::Client
|
|
9
|
+
INSTRUMENTS_ENDPOINT = "/api/user/instruments"
|
|
10
|
+
|
|
11
|
+
def list(token:, user_id:, mobile_number:, options: {})
|
|
12
|
+
ensure_present!(token: token,
|
|
13
|
+
user_id: user_id,
|
|
14
|
+
mobile_number: mobile_number)
|
|
15
|
+
|
|
16
|
+
resp = http.get(INSTRUMENTS_ENDPOINT,
|
|
17
|
+
params: body(user_id, mobile_number, options),
|
|
18
|
+
headers: { "Authorization" => "Bearer #{token}" })
|
|
19
|
+
|
|
20
|
+
# Response is an array of hashes with instrument_token, account_type, description, instrument_alias
|
|
21
|
+
Array(resp).map do |h|
|
|
22
|
+
PaygatePk::Contracts::Instrument.new(
|
|
23
|
+
instrument_token: h["instrument_token"],
|
|
24
|
+
account_type: h["account_type"],
|
|
25
|
+
description: h["description"],
|
|
26
|
+
instrument_alias: h["instrument_alias"],
|
|
27
|
+
raw: h
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def base_url
|
|
35
|
+
config.api_base_url
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def body(user_id, mobile_no, options)
|
|
39
|
+
attrs = {
|
|
40
|
+
"merchant_user_id" => user_id,
|
|
41
|
+
"user_mobile_number" => mobile_no
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# rubocop:disable Naming/VariableNumber
|
|
45
|
+
attrs["customer_ip"] = options[:customer_ip] if options[:customer_ip]
|
|
46
|
+
attrs["reserved_1"] = options[:reserved_1] if options[:reserved_1]
|
|
47
|
+
attrs["reserved_2"] = options[:reserved_2] if options[:reserved_2]
|
|
48
|
+
attrs["reserved_3"] = options[:reserved_3] if options[:reserved_3]
|
|
49
|
+
attrs["api_version"] = options[:api_version] if options[:api_version]
|
|
50
|
+
# rubocop:enable Naming/VariableNumber
|
|
51
|
+
|
|
52
|
+
attrs
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def ensure_present!(**pairs)
|
|
56
|
+
missing = pairs.select { |_k, v| v.nil? || (v.respond_to?(:empty?) && v.empty?) }.keys
|
|
57
|
+
raise PaygatePk::ValidationError, "missing required args: #{missing.join(", ")}" unless missing.empty?
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bigdecimal"
|
|
4
|
+
|
|
5
|
+
module PaygatePk
|
|
6
|
+
module Providers
|
|
7
|
+
module PayFast
|
|
8
|
+
module Tokenization
|
|
9
|
+
# used to generate the bearer_token
|
|
10
|
+
class Token < PaygatePk::Providers::PayFast::Client
|
|
11
|
+
TOKEN_ENDPOINT = "/api/token"
|
|
12
|
+
DEFAULT_GRANT_TYPE = "client_credentials"
|
|
13
|
+
|
|
14
|
+
# 3.1 Authentication Access Token
|
|
15
|
+
# Required: merchant_id, secured_key, grant_type
|
|
16
|
+
# Optional: customer_ip, reserved_1..3, api_version
|
|
17
|
+
# Returns: PaygatePk::Contracts::BearerToken
|
|
18
|
+
def get(grant_type: DEFAULT_GRANT_TYPE, options: {})
|
|
19
|
+
mid = config.merchant_id
|
|
20
|
+
sec = config.secured_key
|
|
21
|
+
|
|
22
|
+
ensure_present!(merchant_id: mid, secured_key: sec, grant_type: grant_type)
|
|
23
|
+
|
|
24
|
+
resp = http.post(TOKEN_ENDPOINT, form: body(mid, sec, grant_type, options))
|
|
25
|
+
|
|
26
|
+
PaygatePk::Contracts::BearerToken.new(
|
|
27
|
+
access_token: resp["token"], # if present
|
|
28
|
+
refresh_token: resp["refresh_token"], # shown in doc example
|
|
29
|
+
expiry: resp["expiry"],
|
|
30
|
+
code: resp["code"],
|
|
31
|
+
message: resp["message"],
|
|
32
|
+
raw: resp
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def base_url
|
|
39
|
+
config.api_base_url
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def body(mid, sec, grant_type, options)
|
|
43
|
+
attrs = {
|
|
44
|
+
"merchant_id" => mid,
|
|
45
|
+
"secured_key" => sec,
|
|
46
|
+
"grant_type" => grant_type
|
|
47
|
+
}
|
|
48
|
+
attrs["customer_ip"] = options[:customer_ip] if options[:customer_ip]
|
|
49
|
+
attrs["reserved_1"] = options[:reserved1] if options[:reserved1]
|
|
50
|
+
attrs["reserved_2"] = options[:reserved2] if options[:reserved2]
|
|
51
|
+
attrs["reserved_3"] = options[:reserved3] if options[:reserved3]
|
|
52
|
+
attrs["api_version"] = options[:api_version] if options[:api_version]
|
|
53
|
+
|
|
54
|
+
attrs
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def ensure_present!(**pairs)
|
|
58
|
+
missing = pairs.select { |_k, v| v.nil? || (v.respond_to?(:empty?) && v.empty?) }.keys
|
|
59
|
+
raise PaygatePk::ValidationError, "missing required args: #{missing.join(", ")}" unless missing.empty?
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PaygatePk
|
|
4
|
+
module Providers
|
|
5
|
+
module PayFast
|
|
6
|
+
# Verifies PayFast IPN/notification params (GET to your CHECKOUT_URL)
|
|
7
|
+
# Returns PaygatePk::Contracts::WebhookEvent on success, raises on failure.
|
|
8
|
+
class Webhook
|
|
9
|
+
def verify!(raw_params)
|
|
10
|
+
params = normalize_keys(raw_params)
|
|
11
|
+
|
|
12
|
+
validate_required_params(params)
|
|
13
|
+
verify_signature(params)
|
|
14
|
+
build_webhook(params, raw_params)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def verify_signature(params)
|
|
20
|
+
expected = PaygatePk::Util::Signature::Payfast.validation_hash(
|
|
21
|
+
basket_id: params["basket_id"],
|
|
22
|
+
merchant_secret_key: PaygatePk.config.pay_fast.secured_key,
|
|
23
|
+
merchant_id: PaygatePk.config.pay_fast.merchant_id,
|
|
24
|
+
payfast_err_code: params["err_code"]
|
|
25
|
+
)
|
|
26
|
+
return if PaygatePk::Util::Security.secure_compare(expected, params["validation_hash"])
|
|
27
|
+
|
|
28
|
+
raise PaygatePk::SignatureError, "invalid validation_hash"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def validate_required_params(params)
|
|
32
|
+
%w[basket_id err_code validation_hash].each do |k|
|
|
33
|
+
raise PaygatePk::SignatureError, "missing #{k}" unless present?(params[k])
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def normalize_keys(hash)
|
|
38
|
+
hash.transform_keys(&:to_s).tap do |x|
|
|
39
|
+
# common aliases to lowercase
|
|
40
|
+
x["instrument_token"] ||= x["Instrument_token"]
|
|
41
|
+
x["recurring_txn"] ||= x["Recurring_txn"] || x["RECURRING_TXN"]
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def present?(val)
|
|
46
|
+
!(val.nil? || (val.respond_to?(:empty?) && val.empty?))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def truthy?(val)
|
|
50
|
+
[true, "true", "TRUE", "1", 1].include?(val)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def build_webhook(params, raw_params)
|
|
54
|
+
PaygatePk::Contracts::WebhookEvent.new(
|
|
55
|
+
provider: :payfast,
|
|
56
|
+
transaction_id: params["transaction_id"],
|
|
57
|
+
basket_id: params["basket_id"],
|
|
58
|
+
order_date: params["order_date"],
|
|
59
|
+
approved: params["err_code"] == "000",
|
|
60
|
+
code: params["err_code"],
|
|
61
|
+
message: params["err_msg"],
|
|
62
|
+
amount: params["amount"],
|
|
63
|
+
currency: params["currency"],
|
|
64
|
+
instrument_token: params["instrument_token"] || params["Instrument_token"],
|
|
65
|
+
recurring: truthy?(params["recurring_txn"]) || truthy?(params["RECURRING_TXN"]),
|
|
66
|
+
raw: raw_params
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# lib/paygate_pk/util/security.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "openssl"
|
|
5
|
+
|
|
6
|
+
module PaygatePk
|
|
7
|
+
module Util
|
|
8
|
+
# Constant-time string compare without Rack/ActiveSupport
|
|
9
|
+
module Security
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# Constant-time string compare without Rack/ActiveSupport
|
|
13
|
+
def secure_compare(expected_hash, incoming_hash)
|
|
14
|
+
return false unless expected_hash.is_a?(String) && incoming_hash.is_a?(String)
|
|
15
|
+
return false unless expected_hash.bytesize == incoming_hash.bytesize
|
|
16
|
+
|
|
17
|
+
OpenSSL.fixed_length_secure_compare(expected_hash, incoming_hash)
|
|
18
|
+
rescue NoMethodError
|
|
19
|
+
# Fallback if Ruby/OpenSSL is too old (very rare on modern Ruby)
|
|
20
|
+
# XOR-based constant-time fallback
|
|
21
|
+
diff = 0
|
|
22
|
+
expected_hash.bytes.zip(incoming_hash.bytes) { |x, y| diff |= (x ^ y) }
|
|
23
|
+
diff.zero?
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# lib/paygate_pk/util/signature.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "openssl"
|
|
5
|
+
|
|
6
|
+
module PaygatePk
|
|
7
|
+
module Util
|
|
8
|
+
module Signature
|
|
9
|
+
# validation_hash = SHA256("basket_id|merchant_secret_key|merchant_id|payfast_err_code")
|
|
10
|
+
module Payfast
|
|
11
|
+
def self.validation_hash(basket_id:, merchant_secret_key:, merchant_id:, payfast_err_code:)
|
|
12
|
+
data = [basket_id, merchant_secret_key, merchant_id, payfast_err_code].join("|")
|
|
13
|
+
OpenSSL::Digest::SHA256.hexdigest(data)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
data/lib/paygate_pk/version.rb
CHANGED
data/lib/paygate_pk.rb
CHANGED
|
@@ -8,13 +8,19 @@ require_relative "paygate_pk/http/client"
|
|
|
8
8
|
# Contracts used by the endpoint
|
|
9
9
|
require_relative "paygate_pk/contracts/access_token"
|
|
10
10
|
require_relative "paygate_pk/contracts/hosted_checkout"
|
|
11
|
+
require_relative "paygate_pk/contracts/bearer_token"
|
|
12
|
+
require_relative "paygate_pk/contracts/instrument"
|
|
13
|
+
require_relative "paygate_pk/contracts/webhook_event"
|
|
11
14
|
|
|
12
15
|
# PayFast
|
|
13
16
|
require_relative "paygate_pk/providers/pay_fast/client"
|
|
14
17
|
require_relative "paygate_pk/providers/pay_fast/auth"
|
|
15
18
|
require_relative "paygate_pk/providers/pay_fast/checkout"
|
|
19
|
+
require_relative "paygate_pk/providers/pay_fast/tokenization/token"
|
|
16
20
|
|
|
17
21
|
require_relative "paygate_pk/util/html"
|
|
22
|
+
require_relative "paygate_pk/util/signature"
|
|
23
|
+
require_relative "paygate_pk/util/security"
|
|
18
24
|
|
|
19
25
|
# Main module for PaygatePk
|
|
20
26
|
module PaygatePk
|
data/paygate_pk.gemspec
CHANGED
|
@@ -35,10 +35,10 @@ Gem::Specification.new do |spec|
|
|
|
35
35
|
spec.add_dependency "faraday", ">= 2.7"
|
|
36
36
|
spec.add_dependency "faraday-retry", ">= 2.0"
|
|
37
37
|
spec.add_dependency "json"
|
|
38
|
+
spec.add_dependency "nokogiri", ">= 1.16", "< 2.0"
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
spec.add_development_dependency "byebug"
|
|
40
41
|
spec.add_development_dependency "minitest", "~> 5.0"
|
|
41
|
-
spec.add_dependency "nokogiri", ">= 1.16", "< 2.0"
|
|
42
42
|
spec.add_development_dependency "rake", "~> 13.0"
|
|
43
43
|
spec.add_development_dependency "rubocop", "~> 1.21"
|
|
44
44
|
spec.add_development_dependency "simplecov", ">= 0.22"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: paygate_pk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Talha Junaid
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-10-
|
|
11
|
+
date: 2025-10-10 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -52,20 +52,6 @@ dependencies:
|
|
|
52
52
|
- - ">="
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
54
|
version: '0'
|
|
55
|
-
- !ruby/object:Gem::Dependency
|
|
56
|
-
name: minitest
|
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
|
58
|
-
requirements:
|
|
59
|
-
- - "~>"
|
|
60
|
-
- !ruby/object:Gem::Version
|
|
61
|
-
version: '5.0'
|
|
62
|
-
type: :development
|
|
63
|
-
prerelease: false
|
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
-
requirements:
|
|
66
|
-
- - "~>"
|
|
67
|
-
- !ruby/object:Gem::Version
|
|
68
|
-
version: '5.0'
|
|
69
55
|
- !ruby/object:Gem::Dependency
|
|
70
56
|
name: nokogiri
|
|
71
57
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -86,6 +72,34 @@ dependencies:
|
|
|
86
72
|
- - "<"
|
|
87
73
|
- !ruby/object:Gem::Version
|
|
88
74
|
version: '2.0'
|
|
75
|
+
- !ruby/object:Gem::Dependency
|
|
76
|
+
name: byebug
|
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
82
|
+
type: :development
|
|
83
|
+
prerelease: false
|
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0'
|
|
89
|
+
- !ruby/object:Gem::Dependency
|
|
90
|
+
name: minitest
|
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '5.0'
|
|
96
|
+
type: :development
|
|
97
|
+
prerelease: false
|
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - "~>"
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '5.0'
|
|
89
103
|
- !ruby/object:Gem::Dependency
|
|
90
104
|
name: rake
|
|
91
105
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -162,13 +176,21 @@ files:
|
|
|
162
176
|
- lib/paygate_pk.rb
|
|
163
177
|
- lib/paygate_pk/config.rb
|
|
164
178
|
- lib/paygate_pk/contracts/access_token.rb
|
|
179
|
+
- lib/paygate_pk/contracts/bearer_token.rb
|
|
165
180
|
- lib/paygate_pk/contracts/hosted_checkout.rb
|
|
181
|
+
- lib/paygate_pk/contracts/instrument.rb
|
|
182
|
+
- lib/paygate_pk/contracts/webhook_event.rb
|
|
166
183
|
- lib/paygate_pk/errors.rb
|
|
167
184
|
- lib/paygate_pk/http/client.rb
|
|
168
185
|
- lib/paygate_pk/providers/pay_fast/auth.rb
|
|
169
186
|
- lib/paygate_pk/providers/pay_fast/checkout.rb
|
|
170
187
|
- lib/paygate_pk/providers/pay_fast/client.rb
|
|
188
|
+
- lib/paygate_pk/providers/pay_fast/tokenization/instrument.rb
|
|
189
|
+
- lib/paygate_pk/providers/pay_fast/tokenization/token.rb
|
|
190
|
+
- lib/paygate_pk/providers/pay_fast/webhook.rb
|
|
171
191
|
- lib/paygate_pk/util/html.rb
|
|
192
|
+
- lib/paygate_pk/util/security.rb
|
|
193
|
+
- lib/paygate_pk/util/signature.rb
|
|
172
194
|
- lib/paygate_pk/version.rb
|
|
173
195
|
- paygate_pk.gemspec
|
|
174
196
|
- sig/paygate_pk.rbs
|