paygate_pk 0.2.2 → 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 -22
- 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 -72
- data/lib/paygate_pk/util/html.rb +0 -42
- data/lib/paygate_pk/util/signature.rb +0 -18
- data/paygate_pk.gemspec +0 -46
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9705a5a01a864f4d8e318e11448540887091d6ef3d7294c1abaeb00df24a4cba
|
|
4
|
+
data.tar.gz: d0408482c432003735671608da1a46e71f1bae509c8b7f455d7dcbd1dd99865e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 95ff08faf44d5d6bb64311e24b02ce077ac0e19c28b33ce322acd647f1728cd3702b5af08f42bf89dd9d80f1378ccff424ad510e60e30c78de264d2b580acebb
|
|
7
|
+
data.tar.gz: 43e241ecdaa260a068e5224f0a560845b09a07de9f61cb2cef667796e8611697b50070d55f7d6ddee6a66b431794444ad748bb63128ac498272a86e9cb7a569d
|
data/.rubocop.yml
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
AllCops:
|
|
2
|
-
TargetRubyVersion:
|
|
2
|
+
TargetRubyVersion: 3.1
|
|
3
|
+
NewCops: enable
|
|
4
|
+
SuggestExtensions: false
|
|
5
|
+
Exclude:
|
|
6
|
+
- "coverage/**/*"
|
|
7
|
+
- "vendor/**/*"
|
|
8
|
+
- "*.gemspec"
|
|
3
9
|
|
|
4
10
|
Style/StringLiterals:
|
|
5
11
|
Enabled: true
|
|
@@ -9,16 +15,38 @@ Style/StringLiteralsInInterpolation:
|
|
|
9
15
|
Enabled: true
|
|
10
16
|
EnforcedStyle: double_quotes
|
|
11
17
|
|
|
18
|
+
Style/Documentation:
|
|
19
|
+
Enabled: false
|
|
20
|
+
|
|
12
21
|
Layout/LineLength:
|
|
13
22
|
Max: 120
|
|
14
23
|
|
|
15
24
|
Metrics/MethodLength:
|
|
16
|
-
Max:
|
|
25
|
+
Max: 25
|
|
17
26
|
CountAsOne:
|
|
18
27
|
- array
|
|
19
28
|
- hash
|
|
20
29
|
- heredoc
|
|
21
30
|
- method_call
|
|
31
|
+
Exclude:
|
|
32
|
+
- "test/**/*"
|
|
22
33
|
|
|
23
34
|
Metrics/AbcSize:
|
|
24
|
-
Max:
|
|
35
|
+
Max: 25
|
|
36
|
+
Exclude:
|
|
37
|
+
- "test/**/*"
|
|
38
|
+
|
|
39
|
+
Metrics/ParameterLists:
|
|
40
|
+
CountKeywordArgs: false
|
|
41
|
+
|
|
42
|
+
Metrics/ClassLength:
|
|
43
|
+
Max: 200
|
|
44
|
+
|
|
45
|
+
# PayFast field names use ADDRESS_1, ADDRESS_2 -- mirror them in the
|
|
46
|
+
# option hashes so it's obvious how each key maps to the wire format.
|
|
47
|
+
Naming/VariableNumber:
|
|
48
|
+
Enabled: false
|
|
49
|
+
|
|
50
|
+
Naming/MethodParameterName:
|
|
51
|
+
Exclude:
|
|
52
|
+
- "test/**/*"
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,131 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.0.0] - 2026-05-12
|
|
4
|
+
|
|
5
|
+
### Overview
|
|
6
|
+
|
|
7
|
+
Complete rewrite focused on **PayFast hosted-checkout (redirection)**.
|
|
8
|
+
The gem now ships everything needed to take a one-time payment in three
|
|
9
|
+
files — initializer, controller, and a one-line ERB. The 0.x server-to-
|
|
10
|
+
server `Checkout` class that POSTed to `PostTransaction` has been removed;
|
|
11
|
+
that endpoint is browser-side per PayFast's Merchant Integration Guide,
|
|
12
|
+
and the 0.x implementation was architecturally wrong.
|
|
13
|
+
|
|
14
|
+
### Public API (new)
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
PaygatePk::PayFast::Redirect.build(...) # → Contracts::RedirectRequest
|
|
18
|
+
PaygatePk::PayFast::Callback.verify!(p) # → Contracts::CallbackEvent
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Rails view helper:
|
|
22
|
+
|
|
23
|
+
```erb
|
|
24
|
+
<%= paygate_pk_redirect_form(@redirect, autosubmit: true) %>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- `PaygatePk::PayFast::Redirect` — fetches an access token, then assembles
|
|
30
|
+
every mandatory + optional PayFast form field documented in Merchant
|
|
31
|
+
Integration Guide v2.3 §3.2. Handles `items[]` arrays, shipping/billing
|
|
32
|
+
blocks, recurring flag, store override, currency override, and
|
|
33
|
+
free-form `extra_fields` passthrough.
|
|
34
|
+
- `PaygatePk::PayFast::Callback` — replaces the 0.x `Webhook` class.
|
|
35
|
+
Down-cases all incoming param keys once at entry so PayFast's
|
|
36
|
+
mixed-case `Recurring_txn`/`Instrument_token`/`PaymentName` and
|
|
37
|
+
lower-snake `basket_id`/`err_code`/`validation_hash` all work without
|
|
38
|
+
caller intervention.
|
|
39
|
+
- `PaygatePk::PayFast::Endpoints` — URL map keyed by environment
|
|
40
|
+
(`:sandbox`, `:production`); `c.pay_fast.base_url=` override still
|
|
41
|
+
supported for custom staging hosts.
|
|
42
|
+
- `PaygatePk::Rails::ViewHelpers#paygate_pk_redirect_form` —
|
|
43
|
+
auto-submitting form helper with CSP-nonce support, autoloaded via
|
|
44
|
+
a Railtie when the gem boots inside a Rails app.
|
|
45
|
+
- `Contracts::RedirectRequest` — `{ provider, action_url, http_method,
|
|
46
|
+
fields, basket_id, amount, token, raw }`.
|
|
47
|
+
- `Contracts::CallbackEvent` — universal IPN/return shape with
|
|
48
|
+
`approved?` predicate. Future Easypaisa callbacks will return the
|
|
49
|
+
same struct.
|
|
50
|
+
- `PaygatePk::Coercions` — `to_iso_date`, `to_amount_string`,
|
|
51
|
+
`blank?`/`present?`.
|
|
52
|
+
- `Config::PayFastConfig#environment` toggle, validated setter.
|
|
53
|
+
- `Config::PayFastConfig#merchant_name` (was hardcoded to `""` in 0.x).
|
|
54
|
+
- `HTTP::Client`: full Faraday error mapping (`TimeoutError`,
|
|
55
|
+
`ConnectionError`, `HTTPError` cover-all), memoised connection,
|
|
56
|
+
log redaction for `SECURED_KEY`/`Authorization`/`Credentials`.
|
|
57
|
+
- `Errors::CapabilityNotSupported`, `Errors::TimeoutError`,
|
|
58
|
+
`Errors::ConnectionError`, `Errors::ProviderError`.
|
|
59
|
+
|
|
60
|
+
### Changed
|
|
61
|
+
|
|
62
|
+
- Public namespace flattened from `PaygatePk::Providers::PayFast::*`
|
|
63
|
+
to `PaygatePk::PayFast::*`.
|
|
64
|
+
- `Util::Signature::Payfast` renamed to `Util::Signature::PayFast`.
|
|
65
|
+
- `Config#frozen?` renamed to `Config#configured?`. `Config#freeze!`
|
|
66
|
+
now actually deep-freezes (was a no-op `@frozen = true` boolean).
|
|
67
|
+
- `Contracts::AccessToken#token` is now an alias for `#value`; the
|
|
68
|
+
primary field name is `value` to stay consistent with future
|
|
69
|
+
bearer/charge tokens.
|
|
70
|
+
- Required Ruby version: `>= 3.1.0` (matches the 0.x README claim;
|
|
71
|
+
gemspec used to say `>= 2.6.0` despite Faraday 2 effectively needing
|
|
72
|
+
3.1).
|
|
73
|
+
- `spec.require_paths` reduced from `%w[lib test]` to `["lib"]` so
|
|
74
|
+
test helpers are no longer shipped on the gem load path.
|
|
75
|
+
- `nokogiri` removed from runtime deps (no longer parsing HTML
|
|
76
|
+
responses now that `Checkout` is gone).
|
|
77
|
+
- Test suite rewritten end-to-end. 76 tests / 200 assertions /
|
|
78
|
+
96 % line coverage / 77 % branch coverage.
|
|
79
|
+
|
|
80
|
+
### Removed
|
|
81
|
+
|
|
82
|
+
- `Providers::PayFast::Client` — dual facade/base-class
|
|
83
|
+
identity replaced by namespace module + `Endpoints`.
|
|
84
|
+
- `Providers::PayFast::Checkout` — server-to-server POST
|
|
85
|
+
to `PostTransaction` was wrong for the redirection flow.
|
|
86
|
+
- `Providers::PayFast::Webhook` — renamed to `Callback`.
|
|
87
|
+
- `Providers::PayFast::Tokenization::Token`/`Instrument` — deferred
|
|
88
|
+
to 1.2 alongside the saved-instrument charge endpoint. Source
|
|
89
|
+
preserved in git history.
|
|
90
|
+
- `Util::Html` — no longer needed.
|
|
91
|
+
- `Contracts::HostedCheckout` — replaced by
|
|
92
|
+
`RedirectRequest`.
|
|
93
|
+
- `Contracts::WebhookEvent` — renamed to
|
|
94
|
+
`CallbackEvent` with broader field coverage and an `approved?`
|
|
95
|
+
predicate.
|
|
96
|
+
- `Contracts::BearerToken`/`Instrument` — deferred to 1.2.
|
|
97
|
+
|
|
98
|
+
### Fixed
|
|
99
|
+
|
|
100
|
+
- Callback key-casing bug. The 0.x `Webhook#verify!` only aliased
|
|
101
|
+
two specific PascalCase keys; in production PayFast sends a mix of
|
|
102
|
+
lower-snake and PascalCase keys that the old code couldn't read.
|
|
103
|
+
- `ORDER_DATE` is now coerced to `YYYY-MM-DD` per spec; the 0.x
|
|
104
|
+
flow let timestamped strings through silently.
|
|
105
|
+
- `SIGNATURE` is generated fresh per call (PayFast doc: "A random
|
|
106
|
+
string value"). The 0.x integration in office-management was
|
|
107
|
+
shipping the literal demo string `"SOMERANDOM-STRING"` to
|
|
108
|
+
production.
|
|
109
|
+
- `CURRENCY_CODE` is now plumbed through `Redirect.build`; the 0.x
|
|
110
|
+
`Checkout` read the global `PaygatePk.config.default_currency` and
|
|
111
|
+
ignored per-call values, silently mismatching auth and checkout.
|
|
112
|
+
- All `Faraday::Error` subclasses (timeout, connection failure, 5xx,
|
|
113
|
+
SSL) now surface as typed `PaygatePk` errors instead of escaping
|
|
114
|
+
as raw `Faraday::*` exceptions.
|
|
115
|
+
- `SECURED_KEY` no longer leaks into Rails logs when a debug logger
|
|
116
|
+
is wired up.
|
|
117
|
+
- `Config#freeze!` is now a real deep-freeze.
|
|
118
|
+
|
|
119
|
+
### Migration from 0.x
|
|
120
|
+
|
|
121
|
+
| 0.x | 1.0 |
|
|
122
|
+
|-----|-----|
|
|
123
|
+
| `PaygatePk::Providers::PayFast::Auth.new.get_access_token(...)` | `PaygatePk::PayFast::Redirect.build(...)` calls `Auth` internally — you usually don't need it directly. |
|
|
124
|
+
| `PaygatePk::Providers::PayFast::Checkout.new.create!(opts: {...})` | `PaygatePk::PayFast::Redirect.build(...)` (kwargs, no `opts:` wrapper). |
|
|
125
|
+
| `PaygatePk::Providers::PayFast::Webhook.new.verify!(params)` | `PaygatePk::PayFast::Callback.verify!(params)` |
|
|
126
|
+
| `c.pay_fast.base_url = "..."` (hand-typed sandbox host) | `c.pay_fast.environment = :sandbox` (override only when PayFast hands you a custom staging URL). |
|
|
127
|
+
| Hand-built 18-field hidden form ERB | `<%= paygate_pk_redirect_form(@redirect) %>` |
|
|
128
|
+
|
|
3
129
|
## [0.2.0] - 2025-10-10
|
|
4
130
|
|
|
5
131
|
- Added: IPN verification, Tokenization 3.1 & 3.15.
|
data/Gemfile.lock
CHANGED
|
@@ -1,24 +1,49 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
paygate_pk (0.
|
|
4
|
+
paygate_pk (1.0.0)
|
|
5
5
|
faraday (>= 2.7)
|
|
6
6
|
faraday-retry (>= 2.0)
|
|
7
7
|
json
|
|
8
|
-
nokogiri (>= 1.16, < 2.0)
|
|
9
8
|
|
|
10
9
|
GEM
|
|
11
10
|
remote: https://rubygems.org/
|
|
12
11
|
specs:
|
|
12
|
+
actionview (8.1.3)
|
|
13
|
+
activesupport (= 8.1.3)
|
|
14
|
+
builder (~> 3.1)
|
|
15
|
+
erubi (~> 1.11)
|
|
16
|
+
rails-dom-testing (~> 2.2)
|
|
17
|
+
rails-html-sanitizer (~> 1.6)
|
|
18
|
+
activesupport (8.1.3)
|
|
19
|
+
base64
|
|
20
|
+
bigdecimal
|
|
21
|
+
concurrent-ruby (~> 1.0, >= 1.3.1)
|
|
22
|
+
connection_pool (>= 2.2.5)
|
|
23
|
+
drb
|
|
24
|
+
i18n (>= 1.6, < 2)
|
|
25
|
+
json
|
|
26
|
+
logger (>= 1.4.2)
|
|
27
|
+
minitest (>= 5.1)
|
|
28
|
+
securerandom (>= 0.3)
|
|
29
|
+
tzinfo (~> 2.0, >= 2.0.5)
|
|
30
|
+
uri (>= 0.13.1)
|
|
13
31
|
addressable (2.8.7)
|
|
14
32
|
public_suffix (>= 2.0.2, < 7.0)
|
|
15
33
|
ast (2.4.3)
|
|
34
|
+
base64 (0.3.0)
|
|
16
35
|
bigdecimal (3.2.3)
|
|
36
|
+
builder (3.3.0)
|
|
17
37
|
byebug (12.0.0)
|
|
38
|
+
concurrent-ruby (1.3.6)
|
|
39
|
+
connection_pool (3.0.2)
|
|
18
40
|
crack (1.0.0)
|
|
19
41
|
bigdecimal
|
|
20
42
|
rexml
|
|
43
|
+
crass (1.0.6)
|
|
21
44
|
docile (1.4.1)
|
|
45
|
+
drb (2.2.3)
|
|
46
|
+
erubi (1.13.1)
|
|
22
47
|
faraday (2.13.1)
|
|
23
48
|
faraday-net_http (>= 2.0, < 3.5)
|
|
24
49
|
json
|
|
@@ -28,16 +53,21 @@ GEM
|
|
|
28
53
|
faraday-retry (2.3.2)
|
|
29
54
|
faraday (~> 2.0)
|
|
30
55
|
hashdiff (1.2.1)
|
|
56
|
+
i18n (1.14.8)
|
|
57
|
+
concurrent-ruby (~> 1.0)
|
|
31
58
|
json (2.12.2)
|
|
32
59
|
language_server-protocol (3.17.0.5)
|
|
33
60
|
lint_roller (1.1.0)
|
|
34
61
|
logger (1.7.0)
|
|
62
|
+
loofah (2.25.1)
|
|
63
|
+
crass (~> 1.0.2)
|
|
64
|
+
nokogiri (>= 1.12.0)
|
|
35
65
|
minitest (5.25.5)
|
|
36
66
|
net-http (0.6.0)
|
|
37
67
|
uri
|
|
38
|
-
nokogiri (1.
|
|
68
|
+
nokogiri (1.19.3-x86_64-darwin)
|
|
39
69
|
racc (~> 1.4)
|
|
40
|
-
nokogiri (1.
|
|
70
|
+
nokogiri (1.19.3-x86_64-linux-gnu)
|
|
41
71
|
racc (~> 1.4)
|
|
42
72
|
parallel (1.27.0)
|
|
43
73
|
parser (3.3.8.0)
|
|
@@ -46,6 +76,13 @@ GEM
|
|
|
46
76
|
prism (1.5.1)
|
|
47
77
|
public_suffix (6.0.2)
|
|
48
78
|
racc (1.8.1)
|
|
79
|
+
rails-dom-testing (2.3.0)
|
|
80
|
+
activesupport (>= 5.0.0)
|
|
81
|
+
minitest
|
|
82
|
+
nokogiri (>= 1.6)
|
|
83
|
+
rails-html-sanitizer (1.7.0)
|
|
84
|
+
loofah (~> 2.25)
|
|
85
|
+
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
|
49
86
|
rainbow (3.1.1)
|
|
50
87
|
rake (13.3.0)
|
|
51
88
|
regexp_parser (2.10.0)
|
|
@@ -65,16 +102,19 @@ GEM
|
|
|
65
102
|
parser (>= 3.3.7.2)
|
|
66
103
|
prism (~> 1.4)
|
|
67
104
|
ruby-progressbar (1.13.0)
|
|
105
|
+
securerandom (0.4.1)
|
|
68
106
|
simplecov (0.22.0)
|
|
69
107
|
docile (~> 1.1)
|
|
70
108
|
simplecov-html (~> 0.11)
|
|
71
109
|
simplecov_json_formatter (~> 0.1)
|
|
72
110
|
simplecov-html (0.13.2)
|
|
73
111
|
simplecov_json_formatter (0.1.4)
|
|
112
|
+
tzinfo (2.0.6)
|
|
113
|
+
concurrent-ruby (~> 1.0)
|
|
74
114
|
unicode-display_width (3.1.4)
|
|
75
115
|
unicode-emoji (~> 4.0, >= 4.0.4)
|
|
76
116
|
unicode-emoji (4.0.4)
|
|
77
|
-
uri (1.
|
|
117
|
+
uri (1.1.1)
|
|
78
118
|
webmock (3.25.1)
|
|
79
119
|
addressable (>= 2.8.0)
|
|
80
120
|
crack (>= 0.3.2)
|
|
@@ -85,6 +125,7 @@ PLATFORMS
|
|
|
85
125
|
x86_64-linux
|
|
86
126
|
|
|
87
127
|
DEPENDENCIES
|
|
128
|
+
actionview (>= 7.0)
|
|
88
129
|
byebug
|
|
89
130
|
minitest (~> 5.0)
|
|
90
131
|
paygate_pk!
|
data/README.md
CHANGED
|
@@ -1,118 +1,229 @@
|
|
|
1
1
|
# PaygatePk
|
|
2
2
|
|
|
3
|
-
Unified Ruby
|
|
3
|
+
Unified Ruby/Rails client for Pakistani payment gateways.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**1.0 ships:** PayFast hosted-checkout (redirection flow) and callback verification, with a one-line Rails view helper.
|
|
6
|
+
**1.1 will ship:** Easypaisa REST APIs (Mobile Account, OTC voucher, Inquiry).
|
|
7
|
+
**1.2 will ship:** PayFast tokenization / saved-instrument charge.
|
|
8
|
+
|
|
9
|
+
The gem wraps every PayFast field documented in *Merchant Integration Guide v2.3*, validates required inputs, normalises dates and amounts, and returns plain Struct value objects you can pass around your Rails app.
|
|
6
10
|
|
|
7
11
|
## Requirements
|
|
8
12
|
|
|
9
13
|
- Ruby ≥ 3.1
|
|
10
|
-
- Faraday
|
|
11
|
-
-
|
|
12
|
-
- (Dev) Byebug, SimpleCov, RuboCop — optional
|
|
14
|
+
- Faraday ≥ 2.7
|
|
15
|
+
- (Rails apps) ActionView ≥ 7.0 for the view helper
|
|
13
16
|
|
|
14
17
|
## Installation
|
|
15
18
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
If bundler is not being used to manage dependencies, install the gem by executing:
|
|
19
|
+
```sh
|
|
20
|
+
bundle add paygate_pk
|
|
21
|
+
```
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
Or in a Gemfile:
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
```ruby
|
|
26
|
+
gem "paygate_pk", "~> 1.0"
|
|
27
|
+
```
|
|
25
28
|
|
|
26
|
-
|
|
29
|
+
## Configure
|
|
27
30
|
|
|
28
31
|
```ruby
|
|
29
32
|
# config/initializers/paygate_pk.rb
|
|
30
|
-
|
|
31
33
|
PaygatePk.configure do |c|
|
|
32
34
|
c.default_currency = "PKR"
|
|
33
|
-
c.user_agent = "paygate_pk/#{PaygatePk::VERSION}"
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
c.pay_fast.
|
|
38
|
-
c.pay_fast.
|
|
39
|
-
c.pay_fast.
|
|
40
|
-
c.pay_fast.api_base_url = "https://api.getfrompayfast.com"
|
|
41
|
-
|
|
42
|
-
# Optional: tune timeouts & retries
|
|
43
|
-
|
|
44
|
-
c.timeouts = { open_timeout: 5, read_timeout: 10 }
|
|
45
|
-
c.retry = { max: 2, interval: 0.2, backoff_factor: 2.0, retry_statuses: [429, 500, 502, 503, 504] }
|
|
36
|
+
c.pay_fast.environment = Rails.env.production? ? :production : :sandbox
|
|
37
|
+
c.pay_fast.merchant_id = Rails.application.credentials.dig(:pay_fast, :merchant_id)
|
|
38
|
+
c.pay_fast.secured_key = Rails.application.credentials.dig(:pay_fast, :secured_key)
|
|
39
|
+
c.pay_fast.merchant_name = "Acme Store"
|
|
40
|
+
c.pay_fast.store_id = Rails.application.credentials.dig(:pay_fast, :store_id) # optional
|
|
46
41
|
end
|
|
47
42
|
```
|
|
48
43
|
|
|
49
|
-
|
|
44
|
+
Sandbox URL is built in. If PayFast hands you a bespoke staging or production host, override with `c.pay_fast.base_url = "https://..."`.
|
|
50
45
|
|
|
51
|
-
|
|
46
|
+
After the block runs the config is deep-frozen for the lifetime of the process. Use `PaygatePk.reset_config!` in console/tests to start over.
|
|
52
47
|
|
|
53
|
-
|
|
48
|
+
## Take a one-time payment via PayFast redirect
|
|
54
49
|
|
|
55
|
-
|
|
50
|
+
### 1. Build the redirect
|
|
56
51
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
52
|
+
```ruby
|
|
53
|
+
class Subscription::PaymentsController < ApplicationController
|
|
54
|
+
def create
|
|
55
|
+
@redirect = PaygatePk::PayFast::Redirect.build(
|
|
56
|
+
basket_id: "sp-#{payment.id}",
|
|
57
|
+
amount: 1500, # rupees
|
|
58
|
+
description: "Pro plan — monthly",
|
|
59
|
+
customer: {
|
|
60
|
+
mobile: current_user.mobile, # real 03xx mobile (mandatory)
|
|
61
|
+
email: current_user.email,
|
|
62
|
+
name: current_user.name # optional
|
|
63
|
+
},
|
|
64
|
+
success_url: success_subscription_payments_url,
|
|
65
|
+
failure_url: failed_subscription_payments_url,
|
|
66
|
+
checkout_url: webhooks_pay_fast_url, # optional backend IPN ping
|
|
67
|
+
recurring: false
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
```
|
|
60
72
|
|
|
61
|
-
|
|
73
|
+
### 2. Render the auto-submitting form
|
|
62
74
|
|
|
63
|
-
|
|
75
|
+
```erb
|
|
76
|
+
<%# app/views/subscription/payments/create.html.erb %>
|
|
77
|
+
<%= paygate_pk_redirect_form(@redirect, autosubmit: true) %>
|
|
78
|
+
```
|
|
64
79
|
|
|
65
|
-
)
|
|
80
|
+
The customer's browser is now on PayFast. They pick a bank/card/wallet, enter their OTP, and PayFast redirects them back to your `success_url` (or `failure_url`).
|
|
66
81
|
|
|
67
|
-
|
|
82
|
+
### 3. Verify the return / IPN
|
|
68
83
|
|
|
84
|
+
```ruby
|
|
85
|
+
class Subscription::PaymentsController < ApplicationController
|
|
86
|
+
def success
|
|
87
|
+
event = PaygatePk::PayFast::Callback.verify!(request.parameters)
|
|
88
|
+
if event.approved?
|
|
89
|
+
payment = SubscriptionPayment.find_by(basket_id: event.basket_id)
|
|
90
|
+
payment&.mark_completed!(transaction_id: event.transaction_id, amount: event.amount)
|
|
91
|
+
redirect_to dashboard_path, notice: "Payment received."
|
|
92
|
+
else
|
|
93
|
+
redirect_to dashboard_path, alert: "Payment failed: #{event.message}"
|
|
94
|
+
end
|
|
95
|
+
rescue PaygatePk::SignatureError
|
|
96
|
+
head :bad_request
|
|
97
|
+
end
|
|
98
|
+
end
|
|
69
99
|
```
|
|
70
100
|
|
|
71
|
-
|
|
101
|
+
For the server-to-server IPN (PayFast also POSTs to `CHECKOUT_URL`), wire the same call into your webhook controller — it's the more reliable source of truth (it doesn't depend on the customer's browser making it back).
|
|
72
102
|
|
|
73
|
-
|
|
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)
|
|
103
|
+
## All redirect options
|
|
88
104
|
|
|
105
|
+
```ruby
|
|
106
|
+
PaygatePk::PayFast::Redirect.build(
|
|
107
|
+
basket_id: "B-1001",
|
|
108
|
+
amount: 1500,
|
|
109
|
+
description: "Order #1001",
|
|
110
|
+
customer: { mobile: "03001234567", email: "buyer@x.com", name: "Talha" },
|
|
111
|
+
success_url: "https://app/success",
|
|
112
|
+
failure_url: "https://app/failure",
|
|
113
|
+
|
|
114
|
+
# — optional —
|
|
115
|
+
currency: "PKR", # defaults to config.default_currency
|
|
116
|
+
order_date: Date.today, # Date / Time / String, coerced to YYYY-MM-DD
|
|
117
|
+
checkout_url: "https://app/ipn",
|
|
118
|
+
store_id: "102-ABC", # overrides config.pay_fast.store_id
|
|
119
|
+
recurring: false,
|
|
120
|
+
tran_type: "ECOMM_PURCHASE", # overrides config.pay_fast.tran_type
|
|
121
|
+
processing_type: "HYBRID_TOKEN",
|
|
122
|
+
instrument_token: "tok-from-saved-card",
|
|
123
|
+
transaction_instrument: 3, # 1=bank, 2=UnionPay, 3=card, 4=wallet
|
|
124
|
+
|
|
125
|
+
items: [
|
|
126
|
+
{ sku: "SKU-1", name: "Widget", price: 100, qty: 2 },
|
|
127
|
+
{ sku: "SKU-2", name: "Gizmo", price: 50, qty: 1 }
|
|
128
|
+
],
|
|
129
|
+
|
|
130
|
+
shipping: {
|
|
131
|
+
name: "Talha", address_1: "House 9", address_2: "St 4",
|
|
132
|
+
state: "Punjab", city: "Lahore", postal_code: "54000", method: "Courier"
|
|
133
|
+
},
|
|
134
|
+
billing: { name: "Talha", city: "Lahore", address_1: "House 9" },
|
|
135
|
+
|
|
136
|
+
country: "PK",
|
|
137
|
+
customer_ip: request.remote_ip,
|
|
138
|
+
merchant_customer_id: current_user.id.to_s,
|
|
139
|
+
merchant_user_agent: request.user_agent,
|
|
140
|
+
|
|
141
|
+
extra_fields: { "CUSTOM_X" => "value" } # forward-compatible passthrough
|
|
142
|
+
)
|
|
89
143
|
```
|
|
90
144
|
|
|
91
|
-
|
|
145
|
+
## Contracts
|
|
146
|
+
|
|
147
|
+
### `Contracts::RedirectRequest`
|
|
148
|
+
|
|
149
|
+
What `Redirect.build` returns, what the view helper consumes.
|
|
150
|
+
|
|
151
|
+
| Field | Type | Notes |
|
|
152
|
+
|---|---|---|
|
|
153
|
+
| `provider` | `Symbol` | `:pay_fast` |
|
|
154
|
+
| `action_url` | `String` | Where the browser POSTs |
|
|
155
|
+
| `http_method` | `Symbol` | `:post` |
|
|
156
|
+
| `fields` | `Hash<String,String>` | Every PayFast form field |
|
|
157
|
+
| `basket_id` | `String` | Echo |
|
|
158
|
+
| `amount` | `String` | Echo |
|
|
159
|
+
| `token` | `String` | The access token |
|
|
160
|
+
| `raw` | `Hash` | Raw token-API response |
|
|
161
|
+
|
|
162
|
+
### `Contracts::CallbackEvent`
|
|
163
|
+
|
|
164
|
+
What `Callback.verify!` returns.
|
|
165
|
+
|
|
166
|
+
| Field | Type | Notes |
|
|
167
|
+
|---|---|---|
|
|
168
|
+
| `provider` | `Symbol` | `:pay_fast` |
|
|
169
|
+
| `transaction_id` | `String?` | |
|
|
170
|
+
| `basket_id` | `String` | |
|
|
171
|
+
| `order_date` | `String` | `YYYY-MM-DD` |
|
|
172
|
+
| `approved` / `approved?` | `Boolean` | `true` iff `err_code == "000"` |
|
|
173
|
+
| `code` | `String` | PayFast `err_code` |
|
|
174
|
+
| `message` | `String` | PayFast `err_msg` |
|
|
175
|
+
| `amount` | `String` | `transaction_amount` |
|
|
176
|
+
| `merchant_amount` | `String` | |
|
|
177
|
+
| `discounted_amount` | `String` | |
|
|
178
|
+
| `currency` | `String` | |
|
|
179
|
+
| `payment_method` | `String` | "card", "account", "wallet" |
|
|
180
|
+
| `instrument_token` | `String?` | |
|
|
181
|
+
| `recurring` | `Boolean` | |
|
|
182
|
+
| `raw` | `Hash` | Original params, unmodified |
|
|
183
|
+
|
|
184
|
+
## Errors
|
|
185
|
+
|
|
186
|
+
All errors inherit from `PaygatePk::Error`:
|
|
187
|
+
|
|
188
|
+
| Class | Raised when |
|
|
189
|
+
|---|---|
|
|
190
|
+
| `ConfigurationError` | Required config is missing (e.g. `merchant_id`) |
|
|
191
|
+
| `ValidationError` | A required method argument is missing or blank (see `#details[:missing]`) |
|
|
192
|
+
| `HTTPError` | Non-2xx response from the gateway (carries `#status`, `#body`) |
|
|
193
|
+
| `TimeoutError` | Network timeout (subclass of `HTTPError`) |
|
|
194
|
+
| `ConnectionError` | DNS / SSL / refused connection (subclass of `HTTPError`) |
|
|
195
|
+
| `AuthError` | Token endpoint returned 2xx but the body had no `ACCESS_TOKEN` |
|
|
196
|
+
| `SignatureError` | Callback `validation_hash` mismatch or required field missing |
|
|
197
|
+
| `CapabilityNotSupported` | Provider asked for a flow it doesn't implement (1.1+) |
|
|
198
|
+
| `ProviderError` | Provider business-rule failure (carries `#code`, `#response`) |
|
|
199
|
+
|
|
200
|
+
## Non-Rails apps
|
|
201
|
+
|
|
202
|
+
The view helper is the only Rails-specific piece — and it's autoloaded only if `Rails::Railtie` is present. Everything else works in plain Ruby / Sinatra / Hanami:
|
|
92
203
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
- PaygatePk::HTTPError — network/HTTP failure (wraps response status & body).
|
|
98
|
-
- PaygatePk::AuthError — auth call succeeded at HTTP level but token missing/invalid in body.
|
|
99
|
-
- PaygatePk::SignatureError — reserved for webhook/IPN verification (upcoming).
|
|
100
|
-
- PaygatePk::ProviderError — reserved for provider business-rule failures.
|
|
204
|
+
```ruby
|
|
205
|
+
redirect = PaygatePk::PayFast::Redirect.build(...)
|
|
206
|
+
# Render redirect.fields as <input type="hidden"> in your own template.
|
|
207
|
+
```
|
|
101
208
|
|
|
102
209
|
## Development
|
|
103
210
|
|
|
104
|
-
|
|
211
|
+
```sh
|
|
212
|
+
bin/setup
|
|
213
|
+
bundle exec rake test # 76 examples, 200 assertions, 0 failures
|
|
214
|
+
bundle exec rubocop
|
|
215
|
+
```
|
|
105
216
|
|
|
106
|
-
|
|
217
|
+
## Roadmap
|
|
218
|
+
|
|
219
|
+
- **1.1** — Easypaisa: Mobile Account, OTC voucher, Inquiry, IPN. New helper `paygate_pk_otc_voucher`. Adds `c.easy_paisa.*` config block.
|
|
220
|
+
- **1.2** — PayFast tokenization: bearer-token auth (`/api/token`), saved instruments (`/api/user/instruments`), charge-against-saved-instrument.
|
|
221
|
+
- **1.x+** — Additional Pakistani gateways (JazzCash, HBL, SafePay) under the same `Contracts::RedirectRequest`/`CallbackEvent` shape so host code stays unchanged.
|
|
107
222
|
|
|
108
223
|
## Contributing
|
|
109
224
|
|
|
110
|
-
|
|
225
|
+
Issues and PRs at <https://github.com/qbitechs/paygate_pk>.
|
|
111
226
|
|
|
112
227
|
## License
|
|
113
228
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
## Code of Conduct
|
|
117
|
-
|
|
118
|
-
Everyone interacting in the PaygatePk project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/paygate_pk/blob/master/CODE_OF_CONDUCT.md).
|
|
229
|
+
MIT. See [LICENSE.txt](LICENSE.txt).
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
|
|
5
|
+
module PaygatePk
|
|
6
|
+
# Small set of input normalisers shared across providers.
|
|
7
|
+
# All methods are pure and side-effect-free.
|
|
8
|
+
module Coercions
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
DATE_ISO = "%Y-%m-%d"
|
|
12
|
+
|
|
13
|
+
# Returns "YYYY-MM-DD" (PayFast's ORDER_DATE format) for Date/Time/DateTime
|
|
14
|
+
# or a String already in that shape. nil-tolerant.
|
|
15
|
+
def to_iso_date(value)
|
|
16
|
+
return nil if value.nil?
|
|
17
|
+
return value if value.is_a?(String) && value.match?(/\A\d{4}-\d{2}-\d{2}\z/)
|
|
18
|
+
|
|
19
|
+
case value
|
|
20
|
+
when Date, Time, DateTime then value.strftime(DATE_ISO)
|
|
21
|
+
when String then Date.parse(value).strftime(DATE_ISO)
|
|
22
|
+
else
|
|
23
|
+
raise ArgumentError, "cannot coerce #{value.inspect} to ISO date"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Render a numeric amount as a non-scientific decimal String. PayFast
|
|
28
|
+
# accepts TXNAMT as a string; 1500 → "1500", 1500.5 → "1500.5".
|
|
29
|
+
def to_amount_string(value)
|
|
30
|
+
return nil if value.nil?
|
|
31
|
+
|
|
32
|
+
case value
|
|
33
|
+
when Float, Rational, BigDecimal then format("%g", value)
|
|
34
|
+
else value.to_s
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def blank?(value)
|
|
39
|
+
value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def present?(value)
|
|
43
|
+
!blank?(value)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|