paygate_pk 0.2.3 → 1.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 +4 -4
- data/.rubocop.yml +31 -3
- data/CHANGELOG.md +126 -0
- data/Gemfile.lock +46 -5
- data/README.md +181 -70
- data/lib/paygate_pk/coercions.rb +46 -0
- data/lib/paygate_pk/config.rb +68 -24
- data/lib/paygate_pk/contracts/access_token.rb +8 -2
- data/lib/paygate_pk/contracts/callback_event.rb +38 -0
- data/lib/paygate_pk/contracts/redirect_request.rb +30 -0
- data/lib/paygate_pk/errors.rb +16 -3
- data/lib/paygate_pk/http/client.rb +84 -71
- data/lib/paygate_pk/pay_fast/auth.rb +79 -0
- data/lib/paygate_pk/pay_fast/callback.rb +92 -0
- data/lib/paygate_pk/pay_fast/endpoints.rb +38 -0
- data/lib/paygate_pk/pay_fast/redirect.rb +227 -0
- data/lib/paygate_pk/pay_fast.rb +19 -0
- data/lib/paygate_pk/rails/railtie.rb +19 -0
- data/lib/paygate_pk/rails/view_helpers.rb +59 -0
- data/lib/paygate_pk/util/signature/pay_fast.rb +25 -0
- data/lib/paygate_pk/version.rb +1 -1
- data/lib/paygate_pk.rb +34 -18
- metadata +25 -32
- data/lib/paygate_pk/contracts/bearer_token.rb +0 -18
- data/lib/paygate_pk/contracts/hosted_checkout.rb +0 -8
- data/lib/paygate_pk/contracts/instrument.rb +0 -10
- data/lib/paygate_pk/contracts/webhook_event.rb +0 -24
- data/lib/paygate_pk/providers/pay_fast/auth.rb +0 -61
- data/lib/paygate_pk/providers/pay_fast/checkout.rb +0 -157
- data/lib/paygate_pk/providers/pay_fast/client.rb +0 -53
- data/lib/paygate_pk/providers/pay_fast/tokenization/instrument.rb +0 -63
- data/lib/paygate_pk/providers/pay_fast/tokenization/token.rb +0 -65
- data/lib/paygate_pk/providers/pay_fast/webhook.rb +0 -74
- data/lib/paygate_pk/util/html.rb +0 -42
- data/lib/paygate_pk/util/signature.rb +0 -18
- data/paygate_pk.gemspec +0 -46
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "date"
|
|
4
|
-
|
|
5
|
-
module PaygatePk
|
|
6
|
-
module Providers
|
|
7
|
-
module PayFast
|
|
8
|
-
# Auth client for PayFast API
|
|
9
|
-
class Auth < Client
|
|
10
|
-
ENDPOINT = "/Ecommerce/api/Transaction/GetAccessToken"
|
|
11
|
-
|
|
12
|
-
# Returns Contracts::AccessToken
|
|
13
|
-
# Required by PayFast: MERCHANT_ID, SECURED_KEY, BASKET_ID, TXNAMT, CURRENCY_CODE
|
|
14
|
-
#
|
|
15
|
-
def get_access_token(basket_id:, amount:, currency: PaygatePk.config.default_currency, endpoint: ENDPOINT)
|
|
16
|
-
ensure_config!
|
|
17
|
-
ensure_args!(basket_id: basket_id, amount: amount, currency: currency)
|
|
18
|
-
|
|
19
|
-
# Guide endpoint: .../Ecommerce/api/Transaction/GetAccessToken
|
|
20
|
-
resp = http.post(endpoint,
|
|
21
|
-
form: payload(basket_id, amount, currency))
|
|
22
|
-
token = resp.is_a?(Hash) ? (resp["ACCESS_TOKEN"] || resp["access_token"]) : nil
|
|
23
|
-
raise AuthError, "missing ACCESS_TOKEN in response" unless token
|
|
24
|
-
|
|
25
|
-
Contracts::AccessToken.new(token: token, raw: resp)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
private
|
|
29
|
-
|
|
30
|
-
def base_url
|
|
31
|
-
config.base_url
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def ensure_config!
|
|
35
|
-
missing = []
|
|
36
|
-
missing << :merchant_id if @config.merchant_id.to_s.strip.empty?
|
|
37
|
-
missing << :secured_key if @config.secured_key.to_s.strip.empty?
|
|
38
|
-
raise ConfigurationError, "PayFast config missing: #{missing.join(", ")}" unless missing.empty?
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def ensure_args!(basket_id:, amount:, currency:)
|
|
42
|
-
missing = []
|
|
43
|
-
missing << :basket_id if basket_id.to_s.strip.empty?
|
|
44
|
-
missing << :amount if amount.nil?
|
|
45
|
-
missing << :currency if currency.to_s.strip.empty?
|
|
46
|
-
raise ValidationError, "missing required args: #{missing.join(", ")}" unless missing.empty?
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def payload(basket_id, amount, currency)
|
|
50
|
-
{
|
|
51
|
-
"MERCHANT_ID" => @config.merchant_id,
|
|
52
|
-
"SECURED_KEY" => @config.secured_key,
|
|
53
|
-
"BASKET_ID" => basket_id,
|
|
54
|
-
"TXNAMT" => amount.to_s,
|
|
55
|
-
"CURRENCY_CODE" => currency
|
|
56
|
-
}
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
end
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
# lib/paygate_pk/providers/pay_fast/checkout.rb
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
require "securerandom"
|
|
5
|
-
require "date"
|
|
6
|
-
require "nokogiri"
|
|
7
|
-
|
|
8
|
-
module PaygatePk
|
|
9
|
-
module Providers
|
|
10
|
-
module PayFast
|
|
11
|
-
# Builds and submits a hosted checkout request to PayFast.
|
|
12
|
-
#
|
|
13
|
-
# Usage:
|
|
14
|
-
# client = PaygatePk::Providers::PayFast::Checkout.new(config: PaygatePk.config.payfast)
|
|
15
|
-
# checkout = client.create!(
|
|
16
|
-
# token: token.token,
|
|
17
|
-
# basket_id: "B-1001",
|
|
18
|
-
# amount: 1500,
|
|
19
|
-
# customer: { mobile: "03xxxxxxxxx", email: "buyer@example.com" },
|
|
20
|
-
# success_url: "...",
|
|
21
|
-
# failure_url: "...",
|
|
22
|
-
# description: "Order #1001",
|
|
23
|
-
# checkout_mode: :immediate
|
|
24
|
-
# )
|
|
25
|
-
class Checkout < Client
|
|
26
|
-
ENDPOINT = "/Ecommerce/api/Transaction/PostTransaction"
|
|
27
|
-
|
|
28
|
-
REQUIRED_ROOT_KEYS = %i[token basket_id amount success_url failure_url description].freeze
|
|
29
|
-
REQUIRED_CUSTOMER_KEYS = %i[mobile email].freeze
|
|
30
|
-
|
|
31
|
-
# Creates a hosted checkout via PayFast.
|
|
32
|
-
#
|
|
33
|
-
# @param token [String] ACCESS_TOKEN from GetAccessToken
|
|
34
|
-
# @param basket_id [String]
|
|
35
|
-
# @param amount [Integer, Float] rupees
|
|
36
|
-
# @param customer [Hash] keys: :mobile, :email
|
|
37
|
-
# @param success_url [String]
|
|
38
|
-
# @param failure_url [String]
|
|
39
|
-
# @param description [String] TXNDESC
|
|
40
|
-
# @param order_date [Date] defaults: Date.today
|
|
41
|
-
# @param checkout_mode [Symbol] :immediate or :delayed (defaults from config)
|
|
42
|
-
# @param endpoint [Symbol] default to ENDPOINT constant
|
|
43
|
-
|
|
44
|
-
# @return [PaygatePk::Contracts::HostedCheckout]
|
|
45
|
-
def create!(opts: {})
|
|
46
|
-
validate_config!
|
|
47
|
-
validate_args!(opts)
|
|
48
|
-
|
|
49
|
-
form = build_form(opts)
|
|
50
|
-
|
|
51
|
-
response = http.post(opts[:endpoint] || ENDPOINT, form: form)
|
|
52
|
-
|
|
53
|
-
PaygatePk::Contracts::HostedCheckout.new(
|
|
54
|
-
provider: :payfast,
|
|
55
|
-
basket_id: opts[:basket_id],
|
|
56
|
-
amount: opts[:amount],
|
|
57
|
-
url: PaygatePk::Util::Html.first_anchor_href(response),
|
|
58
|
-
form: PaygatePk::Util::Html.extract_form(response),
|
|
59
|
-
raw: response
|
|
60
|
-
)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
private
|
|
64
|
-
|
|
65
|
-
def base_url
|
|
66
|
-
config.base_url
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# -- Validation ---------------------------------------------------------
|
|
70
|
-
|
|
71
|
-
def validate_config!
|
|
72
|
-
missing = []
|
|
73
|
-
missing << :merchant_id if blank?(config.merchant_id)
|
|
74
|
-
missing << :secured_key if blank?(config.secured_key)
|
|
75
|
-
raise PaygatePk::ConfigurationError, "PayFast config missing: #{missing.join(", ")}" unless missing.empty?
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def validate_args!(opts)
|
|
79
|
-
missing = []
|
|
80
|
-
REQUIRED_ROOT_KEYS.each do |k|
|
|
81
|
-
v = opts[k]
|
|
82
|
-
missing << k if k == :amount ? opts[:amount].nil? : blank?(v)
|
|
83
|
-
end
|
|
84
|
-
REQUIRED_CUSTOMER_KEYS.each { |k| missing << :"customer.#{k}" if blank?(opts[:customer][k]) }
|
|
85
|
-
raise PaygatePk::ValidationError, "missing required args: #{missing.join(", ")}" unless missing.empty?
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
# -- Builders -----------------------------------------------------------
|
|
89
|
-
|
|
90
|
-
def merchant_params
|
|
91
|
-
{
|
|
92
|
-
"MERCHANT_ID" => config.merchant_id,
|
|
93
|
-
"MERCHANT_NAME" => merchant_name_default
|
|
94
|
-
}
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def order_params(opts)
|
|
98
|
-
{
|
|
99
|
-
"BASKET_ID" => opts[:basket_id],
|
|
100
|
-
"TXNDESC" => opts[:description],
|
|
101
|
-
"TXNAMT" => opts[:amount].to_s,
|
|
102
|
-
"CURRENCY_CODE" => PaygatePk.config.default_currency,
|
|
103
|
-
"ORDER_DATE" => (opts[:order_date] || Date.today).to_s,
|
|
104
|
-
"SUCCESS_URL" => opts[:success_url],
|
|
105
|
-
"FAILURE_URL" => opts[:failure_url],
|
|
106
|
-
"CHECKOUT_URL" => normalize_checkout_mode(opts[:checkout_mode] || config.checkout_mode)
|
|
107
|
-
}
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def customer_params(opts)
|
|
111
|
-
{
|
|
112
|
-
"CUSTOMER_MOBILE_NO" => opts[:customer][:mobile],
|
|
113
|
-
"CUSTOMER_EMAIL_ADDRESS" => opts[:customer][:email]
|
|
114
|
-
}
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def generic_params(opts)
|
|
118
|
-
{
|
|
119
|
-
"TOKEN" => opts[:token],
|
|
120
|
-
"PROCCODE" => "00",
|
|
121
|
-
"SIGNATURE" => SecureRandom.hex(16),
|
|
122
|
-
"VERSION" => PaygatePk::VERSION
|
|
123
|
-
}
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def build_form(opts)
|
|
127
|
-
merchant_params
|
|
128
|
-
.merge(order_params(opts))
|
|
129
|
-
.merge(customer_params(opts))
|
|
130
|
-
.merge(generic_params(opts))
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
# -- Helpers ------------------------------------------------------------
|
|
134
|
-
|
|
135
|
-
def normalize_checkout_mode(mode)
|
|
136
|
-
case mode&.to_sym
|
|
137
|
-
when :delayed then "DELAYED"
|
|
138
|
-
when :immediate, nil then "IMMEDIATE"
|
|
139
|
-
else
|
|
140
|
-
# Be lenient but explicit: unknown symbols fall back to IMMEDIATE
|
|
141
|
-
"IMMEDIATE"
|
|
142
|
-
end
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
def merchant_name_default
|
|
146
|
-
# Kept blank as many integrations treat it as optional; override here
|
|
147
|
-
# later if you decide to expose it via config.
|
|
148
|
-
""
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
def blank?(value)
|
|
152
|
-
value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
|
153
|
-
end
|
|
154
|
-
end
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
end
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "../../http/client"
|
|
4
|
-
require_relative "../../version"
|
|
5
|
-
|
|
6
|
-
module PaygatePk
|
|
7
|
-
module Providers
|
|
8
|
-
module PayFast
|
|
9
|
-
# HTTP client for PayFast API
|
|
10
|
-
class Client
|
|
11
|
-
def initialize(config: PaygatePk.config.pay_fast)
|
|
12
|
-
@config = config
|
|
13
|
-
end
|
|
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.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
|
-
|
|
35
|
-
private
|
|
36
|
-
|
|
37
|
-
attr_reader :config
|
|
38
|
-
|
|
39
|
-
def http
|
|
40
|
-
raise ConfigurationError, "PayFast base_url not set" unless config.base_url
|
|
41
|
-
|
|
42
|
-
PaygatePk::HTTP::Client.new(
|
|
43
|
-
base_url: base_url,
|
|
44
|
-
headers: { "Accept" => "application/json" },
|
|
45
|
-
timeouts: PaygatePk.config.timeouts,
|
|
46
|
-
retry_conf: PaygatePk.config.retry,
|
|
47
|
-
logger: PaygatePk.config.logger
|
|
48
|
-
)
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
@@ -1,63 +0,0 @@
|
|
|
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
|
|
@@ -1,65 +0,0 @@
|
|
|
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
|
|
@@ -1,74 +0,0 @@
|
|
|
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["transaction_amount"],
|
|
63
|
-
merchant_amount: params["merchant_amount"],
|
|
64
|
-
payment_method: params["issuer_name"],
|
|
65
|
-
currency: params["currency"],
|
|
66
|
-
instrument_token: params["instrument_token"] || params["Instrument_token"],
|
|
67
|
-
recurring: truthy?(params["recurring_txn"]) || truthy?(params["RECURRING_TXN"]),
|
|
68
|
-
raw: raw_params
|
|
69
|
-
)
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
end
|
data/lib/paygate_pk/util/html.rb
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "nokogiri"
|
|
4
|
-
|
|
5
|
-
module PaygatePk
|
|
6
|
-
module Util
|
|
7
|
-
# HTML parsing utilities
|
|
8
|
-
module Html
|
|
9
|
-
module_function
|
|
10
|
-
|
|
11
|
-
# Extract a specific <form> (default: first) into a hash of action/method/inputs
|
|
12
|
-
def extract_form(html, index: 0)
|
|
13
|
-
doc = parse(html)
|
|
14
|
-
form = doc.css("form")[index]
|
|
15
|
-
return nil unless form
|
|
16
|
-
|
|
17
|
-
{
|
|
18
|
-
action: form["action"],
|
|
19
|
-
method: (form["method"] || "GET").upcase,
|
|
20
|
-
inputs: form.css("input[name]").to_h { |i| [i["name"], i["value"]] }
|
|
21
|
-
}
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
# NEW: Return the first anchor href (or nil) – handy for redirect pages
|
|
25
|
-
# Optionally pass a CSS selector (e.g., "a.pay-button") if you need a specific link.
|
|
26
|
-
def first_anchor_href(html, selector: "a")
|
|
27
|
-
doc = parse(html)
|
|
28
|
-
a = doc.at(selector)
|
|
29
|
-
a ? a["href"] : nil
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# --- internals ----------------------------------------------------------
|
|
33
|
-
|
|
34
|
-
def parse(html)
|
|
35
|
-
Nokogiri::HTML5(html)
|
|
36
|
-
rescue NoMethodError
|
|
37
|
-
# Fallback for environments without HTML5 parser
|
|
38
|
-
Nokogiri::HTML(html)
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
|
@@ -1,18 +0,0 @@
|
|
|
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/paygate_pk.gemspec
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "lib/paygate_pk/version"
|
|
4
|
-
|
|
5
|
-
Gem::Specification.new do |spec|
|
|
6
|
-
spec.name = "paygate_pk"
|
|
7
|
-
spec.version = PaygatePk::VERSION
|
|
8
|
-
spec.authors = ["Talha Junaid"]
|
|
9
|
-
spec.email = ["talhajunaid65@gmail.com"]
|
|
10
|
-
spec.summary = "Unified Ruby wrapper for PayFast"
|
|
11
|
-
spec.description = "Provider-agnostic Ruby/Rails client for PayFast: checkout, webhooks/IPN verification,
|
|
12
|
-
tokenized & recurring payments."
|
|
13
|
-
spec.license = "MIT"
|
|
14
|
-
spec.homepage = "https://github.com/qbitechs/paygate_pk"
|
|
15
|
-
spec.metadata = {
|
|
16
|
-
"source_code_uri" => "https://github.com/qbitechs/paygate_pk",
|
|
17
|
-
"changelog_uri" => "https://github.com/qbitechs/paygate_pk/blob/main/CHANGELOG.md",
|
|
18
|
-
"homepage_url" => spec.homepage
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
spec.required_ruby_version = ">= 2.6.0"
|
|
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(__dir__) do
|
|
26
|
-
`git ls-files -z`.split("\x0").reject do |f|
|
|
27
|
-
(File.expand_path(f) == __FILE__) || f.start_with?(*%w[bin/ test/ spec/ features/ .git .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 = %w[lib test]
|
|
33
|
-
|
|
34
|
-
# Runtime deps
|
|
35
|
-
spec.add_dependency "faraday", ">= 2.7"
|
|
36
|
-
spec.add_dependency "faraday-retry", ">= 2.0"
|
|
37
|
-
spec.add_dependency "json"
|
|
38
|
-
spec.add_dependency "nokogiri", ">= 1.16", "< 2.0"
|
|
39
|
-
|
|
40
|
-
spec.add_development_dependency "byebug"
|
|
41
|
-
spec.add_development_dependency "minitest", "~> 5.0"
|
|
42
|
-
spec.add_development_dependency "rake", "~> 13.0"
|
|
43
|
-
spec.add_development_dependency "rubocop", "~> 1.21"
|
|
44
|
-
spec.add_development_dependency "simplecov", ">= 0.22"
|
|
45
|
-
spec.add_development_dependency "webmock"
|
|
46
|
-
end
|