paygate_pk 1.0.0 → 1.1.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 +3 -0
- data/CHANGELOG.md +87 -0
- data/Gemfile.lock +1 -1
- data/README.md +124 -8
- data/lib/paygate_pk/coercions.rb +19 -1
- data/lib/paygate_pk/config.rb +46 -2
- data/lib/paygate_pk/contracts/charge_result.rb +52 -0
- data/lib/paygate_pk/contracts/inquiry_result.rb +58 -0
- data/lib/paygate_pk/easy_paisa/client.rb +64 -0
- data/lib/paygate_pk/easy_paisa/endpoints.rb +34 -0
- data/lib/paygate_pk/easy_paisa/inquiry.rb +87 -0
- data/lib/paygate_pk/easy_paisa/mobile_account.rb +123 -0
- data/lib/paygate_pk/easy_paisa/otc.rb +146 -0
- data/lib/paygate_pk/easy_paisa.rb +21 -0
- data/lib/paygate_pk/rails/view_helpers.rb +100 -0
- data/lib/paygate_pk/util/credentials.rb +27 -0
- data/lib/paygate_pk/version.rb +1 -1
- data/lib/paygate_pk.rb +20 -0
- metadata +10 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dd62d57ff115a50c39bc303dbc2713554f220d64e7bfa262f5973851ccbef217
|
|
4
|
+
data.tar.gz: 9e459c21d4d178b2601c5ccc37b245defce997995e31bf85e82b5f08ceed9758
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6ad4cb08e2ced98d4daaad9bf4f519615b773dacdca507b9e5eaeee0b03cc9f4cb29eb87e62aee815cfb99c99167e71176c63362d3fa8e20b0d1a82dfe8ac112
|
|
7
|
+
data.tar.gz: fd859b7d8ef82f97e4a34b02dcd3b7b077b6cf5f479069d75194dd2bbd9587beb629cad6bf9e7d8573a0ce4cde4c1e80965d868fcb8489f4eddfb8eb3baf09b5
|
data/.rubocop.yml
CHANGED
|
@@ -42,6 +42,9 @@ Metrics/ParameterLists:
|
|
|
42
42
|
Metrics/ClassLength:
|
|
43
43
|
Max: 200
|
|
44
44
|
|
|
45
|
+
Metrics/ModuleLength:
|
|
46
|
+
Max: 150
|
|
47
|
+
|
|
45
48
|
# PayFast field names use ADDRESS_1, ADDRESS_2 -- mirror them in the
|
|
46
49
|
# option hashes so it's obvious how each key maps to the wire format.
|
|
47
50
|
Naming/VariableNumber:
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,92 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.1.0] - 2026-05-12
|
|
4
|
+
|
|
5
|
+
### Overview
|
|
6
|
+
|
|
7
|
+
Easypaisa lands — three new REST endpoints + a Rails OTC voucher helper.
|
|
8
|
+
Pure addition; nothing in 1.0 changes shape, no breaking changes.
|
|
9
|
+
|
|
10
|
+
### Public API (new)
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
# Mobile Account: customer authorises via OTP push to their Easypaisa wallet
|
|
14
|
+
result = PaygatePk::EasyPaisa::MobileAccount.charge(
|
|
15
|
+
order_id: "lawzo-#{payment.id}",
|
|
16
|
+
amount: 1500,
|
|
17
|
+
mobile_account_no: "03001234567",
|
|
18
|
+
email: "client@example.com"
|
|
19
|
+
)
|
|
20
|
+
result.success? # response_code == "0000"
|
|
21
|
+
result.transaction_id # Ericsson EWP ID
|
|
22
|
+
|
|
23
|
+
# OTC voucher: customer pays cash at any Easypaisa shop
|
|
24
|
+
voucher = PaygatePk::EasyPaisa::OTC.create(
|
|
25
|
+
order_id: "lawzo-#{payment.id}",
|
|
26
|
+
amount: 1500,
|
|
27
|
+
msisdn: "03001234567",
|
|
28
|
+
email: "client@example.com",
|
|
29
|
+
token_expiry: 7.days.from_now
|
|
30
|
+
)
|
|
31
|
+
voucher.payment_token # "40933012" -- show this to the customer
|
|
32
|
+
voucher.expires_at # Time
|
|
33
|
+
|
|
34
|
+
# Inquiry: poll status until PAID
|
|
35
|
+
status = PaygatePk::EasyPaisa::Inquiry.fetch(
|
|
36
|
+
order_id: "lawzo-...",
|
|
37
|
+
account_num: PaygatePk.config.easy_paisa.account_num
|
|
38
|
+
)
|
|
39
|
+
status.paid?
|
|
40
|
+
status.pending?
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Rails OTC voucher helper:
|
|
44
|
+
|
|
45
|
+
```erb
|
|
46
|
+
<%= paygate_pk_otc_voucher(@voucher) %>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
|
|
51
|
+
- `PaygatePk::EasyPaisa::MobileAccount.charge` — `initiate-ma-transaction`.
|
|
52
|
+
- `PaygatePk::EasyPaisa::OTC.create` — `initiate-otc-transaction`. Accepts
|
|
53
|
+
Date/Time/DateTime/String for `token_expiry`, formats to
|
|
54
|
+
`"yyyymmdd HHmmss"` internally, validates future-dated up-front.
|
|
55
|
+
- `PaygatePk::EasyPaisa::Inquiry.fetch` — `inquire-transaction`. Returns
|
|
56
|
+
`InquiryResult` with `paid?` / `failed?` / `pending?` / `expired?` /
|
|
57
|
+
`blocked?` / `reversed?` predicates covering every documented
|
|
58
|
+
`transactionStatus` value.
|
|
59
|
+
- `PaygatePk::EasyPaisa::Endpoints` — sandbox URL baked in;
|
|
60
|
+
`c.easy_paisa.base_url = "..."` override for production (Easypaisa
|
|
61
|
+
hands the host out at go-live).
|
|
62
|
+
- `Contracts::ChargeResult` — universal value object for any REST-style
|
|
63
|
+
"take a payment" call. Carries `success?`, `failed?`, `otc?`,
|
|
64
|
+
`mobile_account?` predicates.
|
|
65
|
+
- `Contracts::InquiryResult` — universal value object for status lookups.
|
|
66
|
+
- `Config::EasyPaisaConfig` — `environment` / `username` / `password` /
|
|
67
|
+
`store_id` / `account_num` / `base_url` override.
|
|
68
|
+
- `Util::Credentials.basic(user, pass)` — `Base64.strict_encode64`
|
|
69
|
+
helper for Easypaisa's `Credentials` HTTP header.
|
|
70
|
+
- `Coercions.to_easypaisa_timestamp` — formats Date/Time to the
|
|
71
|
+
`"yyyymmdd HHmmss"` format `tokenExpiry` demands.
|
|
72
|
+
- `PaygatePk::Rails::ViewHelpers#paygate_pk_otc_voucher(charge_result)`
|
|
73
|
+
— prints a styled voucher (token, amount, mobile, expiry, order id,
|
|
74
|
+
instructions). Renders a failure card with `response_message` when
|
|
75
|
+
the upstream call returned a non-`0000` `responseCode`, never a fake
|
|
76
|
+
token.
|
|
77
|
+
|
|
78
|
+
### Deferred to 1.2
|
|
79
|
+
|
|
80
|
+
- `EasyPaisa::Callback.verify!` — Easypaisa's integration guide mentions
|
|
81
|
+
IPN configuration in the merchant portal but does not ship the full
|
|
82
|
+
wire spec. Until that's published, host apps should poll
|
|
83
|
+
`Inquiry.fetch` after issuing an OTC voucher.
|
|
84
|
+
|
|
85
|
+
### Not changed
|
|
86
|
+
|
|
87
|
+
- Nothing in the PayFast surface; 1.0's contracts and call sites are
|
|
88
|
+
unchanged.
|
|
89
|
+
|
|
3
90
|
## [1.0.0] - 2026-05-12
|
|
4
91
|
|
|
5
92
|
### Overview
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
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
|
|
7
|
-
**1.2 will ship:** PayFast tokenization / saved-instrument charge.
|
|
6
|
+
**1.1 ships:** Easypaisa REST APIs — Mobile Account, OTC voucher, Inquiry — plus a Rails OTC voucher helper.
|
|
7
|
+
**1.2 will ship:** PayFast tokenization / saved-instrument charge and Easypaisa IPN verification.
|
|
8
8
|
|
|
9
|
-
The gem wraps every
|
|
9
|
+
The gem wraps every documented field from the *PayFast Merchant Integration Guide v2.3* and the *Easypaisa REST APIs without RSA Integration Guide*, validates required inputs, normalises dates and amounts, and returns plain Struct value objects you can pass around your Rails app.
|
|
10
10
|
|
|
11
11
|
## Requirements
|
|
12
12
|
|
|
@@ -23,7 +23,7 @@ bundle add paygate_pk
|
|
|
23
23
|
Or in a Gemfile:
|
|
24
24
|
|
|
25
25
|
```ruby
|
|
26
|
-
gem "paygate_pk", "~> 1.
|
|
26
|
+
gem "paygate_pk", "~> 1.1"
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
## Configure
|
|
@@ -33,15 +33,23 @@ gem "paygate_pk", "~> 1.0"
|
|
|
33
33
|
PaygatePk.configure do |c|
|
|
34
34
|
c.default_currency = "PKR"
|
|
35
35
|
|
|
36
|
+
# PayFast (hosted checkout / redirection)
|
|
36
37
|
c.pay_fast.environment = Rails.env.production? ? :production : :sandbox
|
|
37
38
|
c.pay_fast.merchant_id = Rails.application.credentials.dig(:pay_fast, :merchant_id)
|
|
38
39
|
c.pay_fast.secured_key = Rails.application.credentials.dig(:pay_fast, :secured_key)
|
|
39
40
|
c.pay_fast.merchant_name = "Acme Store"
|
|
40
41
|
c.pay_fast.store_id = Rails.application.credentials.dig(:pay_fast, :store_id) # optional
|
|
42
|
+
|
|
43
|
+
# Easypaisa (REST: Mobile Account / OTC / Inquiry)
|
|
44
|
+
c.easy_paisa.environment = Rails.env.production? ? :production : :sandbox
|
|
45
|
+
c.easy_paisa.username = Rails.application.credentials.dig(:easy_paisa, :username)
|
|
46
|
+
c.easy_paisa.password = Rails.application.credentials.dig(:easy_paisa, :password)
|
|
47
|
+
c.easy_paisa.store_id = Rails.application.credentials.dig(:easy_paisa, :store_id)
|
|
48
|
+
c.easy_paisa.account_num = Rails.application.credentials.dig(:easy_paisa, :account_num) # required for Inquiry
|
|
41
49
|
end
|
|
42
50
|
```
|
|
43
51
|
|
|
44
|
-
Sandbox
|
|
52
|
+
Sandbox URLs are built in for both providers. If either hands you a bespoke staging or production host, override with `c.pay_fast.base_url = "https://..."` / `c.easy_paisa.base_url = "https://..."`.
|
|
45
53
|
|
|
46
54
|
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.
|
|
47
55
|
|
|
@@ -142,6 +150,72 @@ PaygatePk::PayFast::Redirect.build(
|
|
|
142
150
|
)
|
|
143
151
|
```
|
|
144
152
|
|
|
153
|
+
## Take a payment via Easypaisa (REST)
|
|
154
|
+
|
|
155
|
+
Easypaisa offers two flavours; pick per use case:
|
|
156
|
+
|
|
157
|
+
- **Mobile Account (MA)** — customer authorises in their Easypaisa app via OTP push; merchant gets paid immediately. No redirect, no voucher.
|
|
158
|
+
- **OTC (Over-the-Counter)** — merchant issues a payment token; customer pays cash at any Easypaisa shop within the expiry window.
|
|
159
|
+
|
|
160
|
+
### Mobile Account
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
result = PaygatePk::EasyPaisa::MobileAccount.charge(
|
|
164
|
+
order_id: "lawzo-#{payment.id}",
|
|
165
|
+
amount: 1500,
|
|
166
|
+
mobile_account_no: "03001234567",
|
|
167
|
+
email: "client@example.com",
|
|
168
|
+
optional: { "1" => "tenant-42" } # optional1..5 passthrough
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
if result.success?
|
|
172
|
+
payment.update!(external_transaction_id: result.transaction_id, status: :pending)
|
|
173
|
+
# customer now sees the OTP prompt on their Easypaisa app
|
|
174
|
+
else
|
|
175
|
+
flash[:alert] = "Easypaisa: #{result.response_message} (code #{result.response_code})"
|
|
176
|
+
end
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### OTC voucher
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
voucher = PaygatePk::EasyPaisa::OTC.create(
|
|
183
|
+
order_id: "lawzo-#{payment.id}",
|
|
184
|
+
amount: 1500,
|
|
185
|
+
msisdn: "03001234567",
|
|
186
|
+
email: "client@example.com",
|
|
187
|
+
token_expiry: 7.days.from_now # Date / Time / DateTime / pre-formatted String
|
|
188
|
+
)
|
|
189
|
+
voucher.payment_token # => "40933012" — print or show this
|
|
190
|
+
voucher.expires_at # => Time
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
```erb
|
|
194
|
+
<%# Rails: one-line printable voucher %>
|
|
195
|
+
<%= paygate_pk_otc_voucher(@voucher) %>
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Inquiry (poll status)
|
|
199
|
+
|
|
200
|
+
Use this after issuing an OTC voucher (or for any MA transaction you didn't catch via IPN):
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
status = PaygatePk::EasyPaisa::Inquiry.fetch(
|
|
204
|
+
order_id: "lawzo-#{payment.id}",
|
|
205
|
+
account_num: PaygatePk.config.easy_paisa.account_num # falls back to config
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
case status.transaction_status
|
|
209
|
+
when "PAID" then payment.mark_completed!(amount: status.transaction_amount)
|
|
210
|
+
when "FAILED", "EXPIRED", "BLOCKED", "REVERSED" then payment.mark_failed!(reason: status.transaction_status)
|
|
211
|
+
when "PENDING" then # customer hasn't visited a shop yet -- check again later
|
|
212
|
+
end
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Predicates available on the result: `paid?`, `failed?`, `pending?`, `expired?`, `blocked?`, `reversed?`, `successful_lookup?`.
|
|
216
|
+
|
|
217
|
+
> **Note on IPN.** Easypaisa supports merchant-portal-configured IPN callbacks, but the wire spec isn't published in the REST guide. Until 1.2 ships `EasyPaisa::Callback.verify!`, poll `Inquiry.fetch` from a background job for OTC and on-demand for MA.
|
|
218
|
+
|
|
145
219
|
## Contracts
|
|
146
220
|
|
|
147
221
|
### `Contracts::RedirectRequest`
|
|
@@ -181,6 +255,47 @@ What `Callback.verify!` returns.
|
|
|
181
255
|
| `recurring` | `Boolean` | |
|
|
182
256
|
| `raw` | `Hash` | Original params, unmodified |
|
|
183
257
|
|
|
258
|
+
### `Contracts::ChargeResult` *(Easypaisa MA / OTC)*
|
|
259
|
+
|
|
260
|
+
| Field | Type | Notes |
|
|
261
|
+
|---|---|---|
|
|
262
|
+
| `provider` | `Symbol` | `:easy_paisa` |
|
|
263
|
+
| `basket_id` | `String` | Easypaisa `orderId` echo |
|
|
264
|
+
| `transaction_id` | `String?` | Ericsson EWP id (MA only) |
|
|
265
|
+
| `payment_token` | `String?` | Cash-redemption token (OTC only) |
|
|
266
|
+
| `payment_mode` | `Symbol` | `:mobile_account` or `:otc` |
|
|
267
|
+
| `expires_at` | `Time?` | OTC token expiry |
|
|
268
|
+
| `transacted_at` | `Time?` | |
|
|
269
|
+
| `amount` | `String` | |
|
|
270
|
+
| `currency` | `String` | |
|
|
271
|
+
| `customer` | `Hash` | Echo of submitted customer info |
|
|
272
|
+
| `response_code` | `String` | `"0000"` on success |
|
|
273
|
+
| `response_message` | `String` | `responseDesc` |
|
|
274
|
+
| `success_code` | `String` | `"0000"` — provider's success literal |
|
|
275
|
+
| `raw` | `Hash` | Full provider response |
|
|
276
|
+
|
|
277
|
+
Predicates: `success?`, `failed?`, `otc?`, `mobile_account?`.
|
|
278
|
+
|
|
279
|
+
### `Contracts::InquiryResult` *(Easypaisa Inquire Transaction)*
|
|
280
|
+
|
|
281
|
+
| Field | Type | Notes |
|
|
282
|
+
|---|---|---|
|
|
283
|
+
| `provider` | `Symbol` | `:easy_paisa` |
|
|
284
|
+
| `basket_id` | `String` | Echoed `orderId` |
|
|
285
|
+
| `account_num` | `String` | |
|
|
286
|
+
| `store_id` / `store_name` | `String?` | |
|
|
287
|
+
| `payment_token` | `String?` | OTC only |
|
|
288
|
+
| `transaction_status` | `String` | `PAID`/`FAILED`/`PENDING`/`BLOCKED`/`EXPIRED`/`REVERSED` |
|
|
289
|
+
| `transaction_amount` | `String` | |
|
|
290
|
+
| `transaction_date_time` | `String` | As Easypaisa sends, `dd/MM/yyyy hh:mm a` |
|
|
291
|
+
| `payment_token_expiry` | `String?` | OTC only |
|
|
292
|
+
| `msisdn` | `String?` | |
|
|
293
|
+
| `payment_mode` | `String` | `"MA"` / `"OTC"` / `"CC"` |
|
|
294
|
+
| `response_code` / `response_message` | `String` | |
|
|
295
|
+
| `raw` | `Hash` | Full provider response |
|
|
296
|
+
|
|
297
|
+
Predicates: `successful_lookup?`, `paid?`, `failed?`, `pending?`, `expired?`, `blocked?`, `reversed?`.
|
|
298
|
+
|
|
184
299
|
## Errors
|
|
185
300
|
|
|
186
301
|
All errors inherit from `PaygatePk::Error`:
|
|
@@ -216,9 +331,10 @@ bundle exec rubocop
|
|
|
216
331
|
|
|
217
332
|
## Roadmap
|
|
218
333
|
|
|
219
|
-
- **1.
|
|
220
|
-
- **1.
|
|
221
|
-
- **1.
|
|
334
|
+
- **1.0** ✅ — PayFast hosted-checkout + callback verification.
|
|
335
|
+
- **1.1** ✅ — Easypaisa: Mobile Account, OTC voucher, Inquiry. `paygate_pk_otc_voucher` Rails helper. `c.easy_paisa.*` config block.
|
|
336
|
+
- **1.2** — Easypaisa IPN (`EasyPaisa::Callback.verify!`). PayFast tokenization: bearer-token auth (`/api/token`), saved instruments (`/api/user/instruments`), charge-against-saved-instrument.
|
|
337
|
+
- **1.x+** — Additional Pakistani gateways (JazzCash, HBL, SafePay) under the same `Contracts::RedirectRequest` / `ChargeResult` / `CallbackEvent` shapes so host code stays unchanged.
|
|
222
338
|
|
|
223
339
|
## Contributing
|
|
224
340
|
|
data/lib/paygate_pk/coercions.rb
CHANGED
|
@@ -8,7 +8,8 @@ module PaygatePk
|
|
|
8
8
|
module Coercions
|
|
9
9
|
module_function
|
|
10
10
|
|
|
11
|
-
DATE_ISO
|
|
11
|
+
DATE_ISO = "%Y-%m-%d"
|
|
12
|
+
EASYPAISA_TIMESTAMP = "%Y%m%d %H%M%S"
|
|
12
13
|
|
|
13
14
|
# Returns "YYYY-MM-DD" (PayFast's ORDER_DATE format) for Date/Time/DateTime
|
|
14
15
|
# or a String already in that shape. nil-tolerant.
|
|
@@ -35,6 +36,23 @@ module PaygatePk
|
|
|
35
36
|
end
|
|
36
37
|
end
|
|
37
38
|
|
|
39
|
+
# Returns "yyyymmdd HHmmss" -- Easypaisa's tokenExpiry format
|
|
40
|
+
# (per the REST without RSA Integration Guide, Initiate OTC
|
|
41
|
+
# Transaction request parameters). Accepts Date/Time/DateTime or a
|
|
42
|
+
# String already in the right shape. nil-tolerant.
|
|
43
|
+
def to_easypaisa_timestamp(value)
|
|
44
|
+
return nil if value.nil?
|
|
45
|
+
return value if value.is_a?(String) && value.match?(/\A\d{8} \d{6}\z/)
|
|
46
|
+
|
|
47
|
+
case value
|
|
48
|
+
when Time, DateTime then value.strftime(EASYPAISA_TIMESTAMP)
|
|
49
|
+
when Date then value.to_time.strftime(EASYPAISA_TIMESTAMP)
|
|
50
|
+
when String then DateTime.parse(value).strftime(EASYPAISA_TIMESTAMP)
|
|
51
|
+
else
|
|
52
|
+
raise ArgumentError, "cannot coerce #{value.inspect} to Easypaisa timestamp"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
38
56
|
def blank?(value)
|
|
39
57
|
value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
|
40
58
|
end
|
data/lib/paygate_pk/config.rb
CHANGED
|
@@ -6,17 +6,23 @@ module PaygatePk
|
|
|
6
6
|
# Typical Rails usage:
|
|
7
7
|
# PaygatePk.configure do |c|
|
|
8
8
|
# c.default_currency = "PKR"
|
|
9
|
+
#
|
|
9
10
|
# c.pay_fast.environment = Rails.env.production? ? :production : :sandbox
|
|
10
11
|
# c.pay_fast.merchant_id = ENV["PAYFAST_MERCHANT_ID"]
|
|
11
12
|
# c.pay_fast.secured_key = ENV["PAYFAST_SECURED_KEY"]
|
|
12
13
|
# c.pay_fast.merchant_name = "Acme Store"
|
|
13
|
-
#
|
|
14
|
+
#
|
|
15
|
+
# c.easy_paisa.environment = Rails.env.production? ? :production : :sandbox
|
|
16
|
+
# c.easy_paisa.username = ENV["EASYPAISA_USERNAME"]
|
|
17
|
+
# c.easy_paisa.password = ENV["EASYPAISA_PASSWORD"]
|
|
18
|
+
# c.easy_paisa.store_id = ENV["EASYPAISA_STORE_ID"]
|
|
19
|
+
# c.easy_paisa.account_num = ENV["EASYPAISA_ACCOUNT"]
|
|
14
20
|
# end
|
|
15
21
|
#
|
|
16
22
|
# After `configure`, the config object is frozen — mutating it raises.
|
|
17
23
|
class Config
|
|
18
24
|
attr_accessor :default_currency, :timeouts, :retry, :user_agent, :logger
|
|
19
|
-
attr_reader :pay_fast
|
|
25
|
+
attr_reader :pay_fast, :easy_paisa
|
|
20
26
|
|
|
21
27
|
def initialize
|
|
22
28
|
@default_currency = "PKR"
|
|
@@ -28,6 +34,7 @@ module PaygatePk
|
|
|
28
34
|
@user_agent = "paygate_pk/#{PaygatePk::VERSION}"
|
|
29
35
|
@logger = nil
|
|
30
36
|
@pay_fast = PayFastConfig.new
|
|
37
|
+
@easy_paisa = EasyPaisaConfig.new
|
|
31
38
|
@configured = false
|
|
32
39
|
end
|
|
33
40
|
|
|
@@ -36,6 +43,7 @@ module PaygatePk
|
|
|
36
43
|
def freeze!
|
|
37
44
|
@configured = true
|
|
38
45
|
@pay_fast.freeze
|
|
46
|
+
@easy_paisa.freeze
|
|
39
47
|
freeze
|
|
40
48
|
self
|
|
41
49
|
end
|
|
@@ -86,5 +94,41 @@ module PaygatePk
|
|
|
86
94
|
PaygatePk::PayFast::Endpoints.base_url(environment)
|
|
87
95
|
end
|
|
88
96
|
end
|
|
97
|
+
|
|
98
|
+
# Per-provider config for Easypaisa (REST without RSA).
|
|
99
|
+
#
|
|
100
|
+
# Required for any call: username + password + store_id. account_num
|
|
101
|
+
# is required for Inquiry only (it's the merchant's EWP Account #
|
|
102
|
+
# found on the Profile page of the Easypaisa Merchant Portal).
|
|
103
|
+
class EasyPaisaConfig
|
|
104
|
+
ENVIRONMENTS = %i[sandbox production].freeze
|
|
105
|
+
|
|
106
|
+
attr_accessor :username, :password, :store_id, :account_num, :base_url
|
|
107
|
+
attr_reader :environment
|
|
108
|
+
|
|
109
|
+
def initialize
|
|
110
|
+
@environment = :sandbox
|
|
111
|
+
@username = nil
|
|
112
|
+
@password = nil
|
|
113
|
+
@store_id = nil
|
|
114
|
+
@account_num = nil
|
|
115
|
+
@base_url = nil
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def environment=(env)
|
|
119
|
+
sym = env&.to_sym
|
|
120
|
+
unless ENVIRONMENTS.include?(sym)
|
|
121
|
+
raise ArgumentError,
|
|
122
|
+
"environment must be one of #{ENVIRONMENTS.inspect}, got #{env.inspect}"
|
|
123
|
+
end
|
|
124
|
+
@environment = sym
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def resolved_base_url
|
|
128
|
+
return base_url if base_url
|
|
129
|
+
|
|
130
|
+
PaygatePk::EasyPaisa::Endpoints.base_url(environment)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
89
133
|
end
|
|
90
134
|
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PaygatePk
|
|
4
|
+
module Contracts
|
|
5
|
+
# Returned by every "create a charge / take a payment" call against a
|
|
6
|
+
# REST-style provider (Easypaisa MA + OTC in 1.1; future providers
|
|
7
|
+
# to follow). Universal shape so host apps can branch on
|
|
8
|
+
# payment_mode rather than provider-specific response classes.
|
|
9
|
+
#
|
|
10
|
+
# OTC fills payment_token and expires_at (the customer takes that
|
|
11
|
+
# token to an Easypaisa shop). MA fills transaction_id (the
|
|
12
|
+
# gateway's Ericsson EWP id) and leaves payment_token nil.
|
|
13
|
+
#
|
|
14
|
+
# response_code/response_message are the upstream provider's raw
|
|
15
|
+
# status code and human-readable reason. Compare against
|
|
16
|
+
# success_code for the success predicate -- Easypaisa says "0000",
|
|
17
|
+
# but other providers may say "000" or "OK".
|
|
18
|
+
ChargeResult = Struct.new(
|
|
19
|
+
:provider, # Symbol e.g. :easy_paisa
|
|
20
|
+
:basket_id, # String -- the merchant's reference (Easypaisa orderId)
|
|
21
|
+
:transaction_id, # String or nil (Easypaisa Ericsson EWP id for MA)
|
|
22
|
+
:payment_token, # String or nil (OTC only)
|
|
23
|
+
:payment_mode, # Symbol :mobile_account | :otc
|
|
24
|
+
:expires_at, # Time or nil (OTC only)
|
|
25
|
+
:transacted_at, # Time or nil
|
|
26
|
+
:amount, # String -- echo of input amount
|
|
27
|
+
:currency, # String
|
|
28
|
+
:customer, # Hash echo of customer info actually sent
|
|
29
|
+
:response_code, # String e.g. "0000"
|
|
30
|
+
:response_message, # String e.g. "SUCCESS"
|
|
31
|
+
:success_code, # String -- provider's success literal
|
|
32
|
+
:raw, # Hash -- full provider response
|
|
33
|
+
keyword_init: true
|
|
34
|
+
) do
|
|
35
|
+
def success?
|
|
36
|
+
response_code == success_code
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def failed?
|
|
40
|
+
!success?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def otc?
|
|
44
|
+
payment_mode == :otc
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def mobile_account?
|
|
48
|
+
payment_mode == :mobile_account
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PaygatePk
|
|
4
|
+
module Contracts
|
|
5
|
+
# Returned by status-inquiry calls (Easypaisa Inquire Transaction in
|
|
6
|
+
# 1.1; future providers to follow). Captures the canonical
|
|
7
|
+
# transaction status alongside everything the upstream API hands
|
|
8
|
+
# back so the host app can rebuild its mental model of the
|
|
9
|
+
# transaction without making additional calls.
|
|
10
|
+
InquiryResult = Struct.new(
|
|
11
|
+
:provider, # Symbol :easy_paisa
|
|
12
|
+
:basket_id, # String Easypaisa orderId
|
|
13
|
+
:account_num, # String Merchant EWP account number
|
|
14
|
+
:store_id, # String
|
|
15
|
+
:store_name, # String
|
|
16
|
+
:payment_token, # String or nil OTC only
|
|
17
|
+
:transaction_status, # String "PAID" | "FAILED" | "PENDING" | "BLOCKED" | "EXPIRED" | "REVERSED"
|
|
18
|
+
:transaction_amount, # String
|
|
19
|
+
:transaction_date_time, # String "dd/MM/yyyy hh:mm a" as Easypaisa sends
|
|
20
|
+
:payment_token_expiry, # String or nil OTC only
|
|
21
|
+
:msisdn, # String
|
|
22
|
+
:payment_mode, # String "MA" | "OTC" | "CC"
|
|
23
|
+
:response_code, # String "0000" on a successful lookup
|
|
24
|
+
:response_message, # String
|
|
25
|
+
:success_code, # String provider's success literal
|
|
26
|
+
:raw, # Hash full provider response
|
|
27
|
+
keyword_init: true
|
|
28
|
+
) do
|
|
29
|
+
def successful_lookup?
|
|
30
|
+
response_code == success_code
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def paid?
|
|
34
|
+
transaction_status == "PAID"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def failed?
|
|
38
|
+
transaction_status == "FAILED"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def pending?
|
|
42
|
+
transaction_status == "PENDING"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def expired?
|
|
46
|
+
transaction_status == "EXPIRED"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def blocked?
|
|
50
|
+
transaction_status == "BLOCKED"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def reversed?
|
|
54
|
+
transaction_status == "REVERSED"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PaygatePk
|
|
4
|
+
module EasyPaisa
|
|
5
|
+
# Internal shared HTTP wrapper for the three Easypaisa REST endpoints.
|
|
6
|
+
# Not advertised on the public surface; MobileAccount / OTC /
|
|
7
|
+
# Inquiry instantiate it directly and consume the parsed response.
|
|
8
|
+
#
|
|
9
|
+
# Responsibilities:
|
|
10
|
+
# - Validate provider config (username + password + store_id) before
|
|
11
|
+
# touching the network; raises ConfigurationError with a list of
|
|
12
|
+
# missing keys.
|
|
13
|
+
# - Build the Credentials header (Base64Strict("user:pass")).
|
|
14
|
+
# - POST a JSON body, returning the decoded Hash.
|
|
15
|
+
# - Convert any non-Hash response (Easypaisa always returns JSON, so
|
|
16
|
+
# this is defensive) into a ProviderError with the raw body
|
|
17
|
+
# attached.
|
|
18
|
+
class Client
|
|
19
|
+
SUCCESS_CODE = "0000"
|
|
20
|
+
|
|
21
|
+
def initialize(config: PaygatePk::EasyPaisa.config, http: nil)
|
|
22
|
+
@config = config
|
|
23
|
+
@http = http
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# POSTs json to the given path. Returns the decoded Hash. Does NOT
|
|
27
|
+
# interpret responseCode -- the caller decides whether non-0000
|
|
28
|
+
# warrants raising vs. surfacing as a result with success? == false.
|
|
29
|
+
def post(path, json:)
|
|
30
|
+
ensure_config!
|
|
31
|
+
http.post(path, json: json, headers: auth_headers)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def success_code
|
|
35
|
+
SUCCESS_CODE
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def http
|
|
41
|
+
@http ||= PaygatePk::HTTP::Client.new(
|
|
42
|
+
base_url: @config.resolved_base_url,
|
|
43
|
+
headers: { "Accept" => "application/json" }
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def auth_headers
|
|
48
|
+
{
|
|
49
|
+
"Credentials" => PaygatePk::Util::Credentials.basic(@config.username, @config.password)
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def ensure_config!
|
|
54
|
+
missing = []
|
|
55
|
+
missing << :username if Coercions.blank?(@config.username)
|
|
56
|
+
missing << :password if Coercions.blank?(@config.password)
|
|
57
|
+
missing << :store_id if Coercions.blank?(@config.store_id)
|
|
58
|
+
return if missing.empty?
|
|
59
|
+
|
|
60
|
+
raise PaygatePk::ConfigurationError, "Easypaisa config missing: #{missing.join(", ")}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PaygatePk
|
|
4
|
+
module EasyPaisa
|
|
5
|
+
# Centralised URL map for Easypaisa REST endpoints. Picks between
|
|
6
|
+
# sandbox and production based on EasyPaisaConfig#environment;
|
|
7
|
+
# callers never hand-type hostnames.
|
|
8
|
+
#
|
|
9
|
+
# The path constants encode the three documented endpoints from the
|
|
10
|
+
# vendor's "REST APIs without RSA Integration Guide".
|
|
11
|
+
module Endpoints
|
|
12
|
+
URLS = {
|
|
13
|
+
sandbox: "https://easypaystg.easypaisa.com.pk",
|
|
14
|
+
# Easypaisa has not published an official production base URL in
|
|
15
|
+
# the public REST guide; merchants typically receive it on
|
|
16
|
+
# go-live. Configure via `c.easy_paisa.base_url = "..."` until
|
|
17
|
+
# then.
|
|
18
|
+
production: "https://easypay.easypaisa.com.pk"
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
INITIATE_MA_PATH = "/easypay-service/rest/v4/initiate-ma-transaction"
|
|
22
|
+
INITIATE_OTC_PATH = "/easypay-service/rest/v4/initiate-otc-transaction"
|
|
23
|
+
INQUIRE_PATH = "/easypay-service/rest/v4/inquire-transaction"
|
|
24
|
+
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
def base_url(env)
|
|
28
|
+
URLS.fetch(env) do
|
|
29
|
+
raise PaygatePk::ConfigurationError, "unknown Easypaisa environment: #{env.inspect}"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PaygatePk
|
|
4
|
+
module EasyPaisa
|
|
5
|
+
# Inquire Transaction Status — given an orderId we previously
|
|
6
|
+
# submitted (and the merchant EWP account number), Easypaisa
|
|
7
|
+
# returns the current state. Use this to drive your own state
|
|
8
|
+
# machine for OTC payments (where success arrives asynchronously
|
|
9
|
+
# when the customer pays at a shop) or as a poor-man's IPN
|
|
10
|
+
# replacement until the IPN spec is wired.
|
|
11
|
+
#
|
|
12
|
+
# POST {base}/easypay-service/rest/v4/inquire-transaction
|
|
13
|
+
#
|
|
14
|
+
# Usage:
|
|
15
|
+
#
|
|
16
|
+
# status = PaygatePk::EasyPaisa::Inquiry.fetch(
|
|
17
|
+
# order_id: "lawzo-#{payment.id}",
|
|
18
|
+
# account_num: PaygatePk.config.easy_paisa.account_num
|
|
19
|
+
# )
|
|
20
|
+
# status.paid? # transactionStatus == "PAID"
|
|
21
|
+
# status.pending? # transactionStatus == "PENDING"
|
|
22
|
+
class Inquiry
|
|
23
|
+
def self.fetch(**kwargs)
|
|
24
|
+
new.fetch(**kwargs)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def initialize(config: PaygatePk::EasyPaisa.config, client: nil)
|
|
28
|
+
@config = config
|
|
29
|
+
@client = client
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def fetch(order_id:, account_num: nil)
|
|
33
|
+
account_num ||= @config.account_num
|
|
34
|
+
ensure_args!(order_id: order_id, account_num: account_num)
|
|
35
|
+
|
|
36
|
+
body = {
|
|
37
|
+
"orderId" => order_id.to_s,
|
|
38
|
+
"storeId" => @config.store_id.to_s,
|
|
39
|
+
"accountNum" => account_num.to_s
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
resp = client.post(Endpoints::INQUIRE_PATH, json: body)
|
|
43
|
+
build_result(order_id, account_num, resp)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def client
|
|
49
|
+
@client ||= Client.new(config: @config)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def ensure_args!(order_id:, account_num:)
|
|
53
|
+
missing = []
|
|
54
|
+
missing << :order_id if Coercions.blank?(order_id)
|
|
55
|
+
missing << :account_num if Coercions.blank?(account_num)
|
|
56
|
+
return if missing.empty?
|
|
57
|
+
|
|
58
|
+
raise PaygatePk::ValidationError.new(
|
|
59
|
+
"missing required args: #{missing.join(", ")}",
|
|
60
|
+
details: { missing: missing }
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def build_result(order_id, account_num, resp)
|
|
65
|
+
resp = {} unless resp.is_a?(Hash)
|
|
66
|
+
Contracts::InquiryResult.new(
|
|
67
|
+
provider: :easy_paisa,
|
|
68
|
+
basket_id: order_id.to_s,
|
|
69
|
+
account_num: account_num.to_s,
|
|
70
|
+
store_id: resp["storeId"]&.to_s,
|
|
71
|
+
store_name: resp["storeName"],
|
|
72
|
+
payment_token: resp["paymentToken"],
|
|
73
|
+
transaction_status: resp["transactionStatus"],
|
|
74
|
+
transaction_amount: resp["transactionAmount"]&.to_s,
|
|
75
|
+
transaction_date_time: resp["transactionDateTime"],
|
|
76
|
+
payment_token_expiry: resp["paymentTokenExpiryDateTime"],
|
|
77
|
+
msisdn: resp["msisdn"],
|
|
78
|
+
payment_mode: resp["paymentMode"],
|
|
79
|
+
response_code: resp["responseCode"],
|
|
80
|
+
response_message: resp["responseDesc"],
|
|
81
|
+
success_code: client.success_code,
|
|
82
|
+
raw: resp
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PaygatePk
|
|
4
|
+
module EasyPaisa
|
|
5
|
+
# Initiate MA Transaction — pushes an OTP confirmation to the
|
|
6
|
+
# customer's Easypaisa wallet so they can authorise payment without
|
|
7
|
+
# leaving the merchant site.
|
|
8
|
+
#
|
|
9
|
+
# POST {base}/easypay-service/rest/v4/initiate-ma-transaction
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
#
|
|
13
|
+
# result = PaygatePk::EasyPaisa::MobileAccount.charge(
|
|
14
|
+
# order_id: "lawzo-#{payment.id}",
|
|
15
|
+
# amount: 1500,
|
|
16
|
+
# mobile_account_no: "03001234567",
|
|
17
|
+
# email: "client@example.com"
|
|
18
|
+
# )
|
|
19
|
+
# result.success? # => true if responseCode == "0000"
|
|
20
|
+
# result.transaction_id # => Ericsson EWP ID (string)
|
|
21
|
+
class MobileAccount
|
|
22
|
+
TRANSACTION_TYPE = "MA"
|
|
23
|
+
|
|
24
|
+
def self.charge(**kwargs)
|
|
25
|
+
new.charge(**kwargs)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def initialize(config: PaygatePk::EasyPaisa.config, client: nil)
|
|
29
|
+
@config = config
|
|
30
|
+
@client = client
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def charge(order_id:, amount:, mobile_account_no:, email:, optional: {})
|
|
34
|
+
ensure_args!(order_id: order_id, amount: amount,
|
|
35
|
+
mobile_account_no: mobile_account_no, email: email)
|
|
36
|
+
|
|
37
|
+
body = build_body(order_id: order_id, amount: amount,
|
|
38
|
+
mobile_account_no: mobile_account_no, email: email,
|
|
39
|
+
optional: optional)
|
|
40
|
+
|
|
41
|
+
resp = client.post(Endpoints::INITIATE_MA_PATH, json: body)
|
|
42
|
+
build_result(order_id: order_id, amount: amount,
|
|
43
|
+
mobile_account_no: mobile_account_no, email: email,
|
|
44
|
+
resp: resp)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def client
|
|
50
|
+
@client ||= Client.new(config: @config)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def ensure_args!(order_id:, amount:, mobile_account_no:, email:)
|
|
54
|
+
missing = []
|
|
55
|
+
missing << :order_id if Coercions.blank?(order_id)
|
|
56
|
+
missing << :amount if amount.nil?
|
|
57
|
+
missing << :mobile_account_no if Coercions.blank?(mobile_account_no)
|
|
58
|
+
missing << :email if Coercions.blank?(email)
|
|
59
|
+
return if missing.empty?
|
|
60
|
+
|
|
61
|
+
raise PaygatePk::ValidationError.new(
|
|
62
|
+
"missing required args: #{missing.join(", ")}",
|
|
63
|
+
details: { missing: missing }
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def build_body(order_id:, amount:, mobile_account_no:, email:, optional:)
|
|
68
|
+
body = {
|
|
69
|
+
"orderId" => order_id.to_s,
|
|
70
|
+
"storeId" => @config.store_id.to_s,
|
|
71
|
+
"transactionAmount" => Coercions.to_amount_string(amount),
|
|
72
|
+
"transactionType" => TRANSACTION_TYPE,
|
|
73
|
+
"mobileAccountNo" => mobile_account_no.to_s,
|
|
74
|
+
"emailAddress" => email.to_s
|
|
75
|
+
}
|
|
76
|
+
add_optionals!(body, optional)
|
|
77
|
+
body
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def add_optionals!(body, optional)
|
|
81
|
+
return unless optional.is_a?(Hash)
|
|
82
|
+
|
|
83
|
+
optional.each do |key, value|
|
|
84
|
+
next if Coercions.blank?(value)
|
|
85
|
+
next unless ("1".."5").cover?(key.to_s)
|
|
86
|
+
|
|
87
|
+
body["optional#{key}"] = value.to_s
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def build_result(order_id:, amount:, mobile_account_no:, email:, resp:)
|
|
92
|
+
resp = {} unless resp.is_a?(Hash)
|
|
93
|
+
Contracts::ChargeResult.new(
|
|
94
|
+
provider: :easy_paisa,
|
|
95
|
+
basket_id: order_id.to_s,
|
|
96
|
+
transaction_id: resp["transactionId"],
|
|
97
|
+
payment_token: nil,
|
|
98
|
+
payment_mode: :mobile_account,
|
|
99
|
+
expires_at: nil,
|
|
100
|
+
transacted_at: parse_easypaisa_time(resp["transactionDateTime"]),
|
|
101
|
+
amount: Coercions.to_amount_string(amount),
|
|
102
|
+
currency: PaygatePk.config.default_currency,
|
|
103
|
+
customer: { mobile_account_no: mobile_account_no.to_s, email: email.to_s },
|
|
104
|
+
response_code: resp["responseCode"],
|
|
105
|
+
response_message: resp["responseDesc"],
|
|
106
|
+
success_code: client.success_code,
|
|
107
|
+
raw: resp
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Easypaisa hands back "dd/MM/yyyy hh:mm a" (e.g. "11/08/2018 11:30 PM").
|
|
112
|
+
# Best-effort parse -- returns the raw string if it's in some
|
|
113
|
+
# unexpected shape rather than blowing up the call site.
|
|
114
|
+
def parse_easypaisa_time(value)
|
|
115
|
+
return nil if Coercions.blank?(value)
|
|
116
|
+
|
|
117
|
+
DateTime.strptime(value, "%d/%m/%Y %I:%M %p").to_time
|
|
118
|
+
rescue ArgumentError, TypeError
|
|
119
|
+
value
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PaygatePk
|
|
4
|
+
module EasyPaisa
|
|
5
|
+
# Initiate OTC Transaction — issues a paymentToken the customer
|
|
6
|
+
# takes to any Easypaisa shop and pays with cash. The merchant gets
|
|
7
|
+
# paid when Easypaisa reconciles the shop deposit (status moves to
|
|
8
|
+
# PAID; poll via Inquiry.fetch or rely on IPN once configured in
|
|
9
|
+
# the merchant portal).
|
|
10
|
+
#
|
|
11
|
+
# POST {base}/easypay-service/rest/v4/initiate-otc-transaction
|
|
12
|
+
#
|
|
13
|
+
# Usage:
|
|
14
|
+
#
|
|
15
|
+
# voucher = PaygatePk::EasyPaisa::OTC.create(
|
|
16
|
+
# order_id: "lawzo-#{payment.id}",
|
|
17
|
+
# amount: 1500,
|
|
18
|
+
# msisdn: "03001234567",
|
|
19
|
+
# email: "client@example.com",
|
|
20
|
+
# token_expiry: 7.days.from_now
|
|
21
|
+
# )
|
|
22
|
+
# voucher.payment_token # => "40933012" (show this to the customer)
|
|
23
|
+
# voucher.expires_at # => Time
|
|
24
|
+
class OTC
|
|
25
|
+
TRANSACTION_TYPE = "OTC"
|
|
26
|
+
|
|
27
|
+
def self.create(**kwargs)
|
|
28
|
+
new.create(**kwargs)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def initialize(config: PaygatePk::EasyPaisa.config, client: nil)
|
|
32
|
+
@config = config
|
|
33
|
+
@client = client
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def create(order_id:, amount:, msisdn:, email:, token_expiry:, optional: {})
|
|
37
|
+
ensure_args!(order_id: order_id, amount: amount,
|
|
38
|
+
msisdn: msisdn, email: email, token_expiry: token_expiry)
|
|
39
|
+
|
|
40
|
+
formatted_expiry = format_expiry(token_expiry)
|
|
41
|
+
|
|
42
|
+
body = build_body(order_id: order_id, amount: amount, msisdn: msisdn,
|
|
43
|
+
email: email, token_expiry: formatted_expiry, optional: optional)
|
|
44
|
+
|
|
45
|
+
resp = client.post(Endpoints::INITIATE_OTC_PATH, json: body)
|
|
46
|
+
build_result(order_id: order_id, amount: amount, msisdn: msisdn,
|
|
47
|
+
email: email, formatted_expiry: formatted_expiry, resp: resp)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def client
|
|
53
|
+
@client ||= Client.new(config: @config)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def ensure_args!(order_id:, amount:, msisdn:, email:, token_expiry:)
|
|
57
|
+
missing = []
|
|
58
|
+
missing << :order_id if Coercions.blank?(order_id)
|
|
59
|
+
missing << :amount if amount.nil?
|
|
60
|
+
missing << :msisdn if Coercions.blank?(msisdn)
|
|
61
|
+
missing << :email if Coercions.blank?(email)
|
|
62
|
+
missing << :token_expiry if Coercions.blank?(token_expiry)
|
|
63
|
+
return if missing.empty?
|
|
64
|
+
|
|
65
|
+
raise PaygatePk::ValidationError.new(
|
|
66
|
+
"missing required args: #{missing.join(", ")}",
|
|
67
|
+
details: { missing: missing }
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def format_expiry(value)
|
|
72
|
+
formatted = Coercions.to_easypaisa_timestamp(value)
|
|
73
|
+
validate_future!(formatted)
|
|
74
|
+
formatted
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Easypaisa returns code "0016" when tokenExpiry is in the past.
|
|
78
|
+
# Surface that as a ValidationError up-front so the caller doesn't
|
|
79
|
+
# waste a round-trip.
|
|
80
|
+
def validate_future!(formatted)
|
|
81
|
+
# Parse back to compare; we trust to_easypaisa_timestamp's shape.
|
|
82
|
+
parsed = DateTime.strptime(formatted, Coercions::EASYPAISA_TIMESTAMP).to_time
|
|
83
|
+
return if parsed > Time.now
|
|
84
|
+
|
|
85
|
+
raise PaygatePk::ValidationError.new(
|
|
86
|
+
"token_expiry must be in the future (got #{formatted})",
|
|
87
|
+
details: { token_expiry: formatted }
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def build_body(order_id:, amount:, msisdn:, email:, token_expiry:, optional:)
|
|
92
|
+
body = {
|
|
93
|
+
"orderId" => order_id.to_s,
|
|
94
|
+
"storeId" => @config.store_id.to_s,
|
|
95
|
+
"transactionAmount" => Coercions.to_amount_string(amount),
|
|
96
|
+
"transactionType" => TRANSACTION_TYPE,
|
|
97
|
+
"msisdn" => msisdn.to_s,
|
|
98
|
+
"emailAddress" => email.to_s,
|
|
99
|
+
"tokenExpiry" => token_expiry
|
|
100
|
+
}
|
|
101
|
+
add_optionals!(body, optional)
|
|
102
|
+
body
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def add_optionals!(body, optional)
|
|
106
|
+
return unless optional.is_a?(Hash)
|
|
107
|
+
|
|
108
|
+
optional.each do |key, value|
|
|
109
|
+
next if Coercions.blank?(value)
|
|
110
|
+
next unless ("1".."5").cover?(key.to_s)
|
|
111
|
+
|
|
112
|
+
body["optional#{key}"] = value.to_s
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def build_result(order_id:, amount:, msisdn:, email:, formatted_expiry:, resp:)
|
|
117
|
+
resp = {} unless resp.is_a?(Hash)
|
|
118
|
+
Contracts::ChargeResult.new(
|
|
119
|
+
provider: :easy_paisa,
|
|
120
|
+
basket_id: order_id.to_s,
|
|
121
|
+
transaction_id: nil,
|
|
122
|
+
payment_token: resp["paymentToken"],
|
|
123
|
+
payment_mode: :otc,
|
|
124
|
+
expires_at: parse_easypaisa_time(resp["paymentTokenExpiryDateTime"]) ||
|
|
125
|
+
DateTime.strptime(formatted_expiry, Coercions::EASYPAISA_TIMESTAMP).to_time,
|
|
126
|
+
transacted_at: parse_easypaisa_time(resp["transactionDateTime"]),
|
|
127
|
+
amount: Coercions.to_amount_string(amount),
|
|
128
|
+
currency: PaygatePk.config.default_currency,
|
|
129
|
+
customer: { msisdn: msisdn.to_s, email: email.to_s },
|
|
130
|
+
response_code: resp["responseCode"],
|
|
131
|
+
response_message: resp["responseDesc"],
|
|
132
|
+
success_code: client.success_code,
|
|
133
|
+
raw: resp
|
|
134
|
+
)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def parse_easypaisa_time(value)
|
|
138
|
+
return nil if Coercions.blank?(value)
|
|
139
|
+
|
|
140
|
+
DateTime.strptime(value, "%d/%m/%Y %I:%M %p").to_time
|
|
141
|
+
rescue ArgumentError, TypeError
|
|
142
|
+
value
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PaygatePk
|
|
4
|
+
# Easypaisa integration (REST APIs without RSA encryption, per the
|
|
5
|
+
# vendor's "REST APIs without RSA Integration Guide").
|
|
6
|
+
#
|
|
7
|
+
# Public API (1.1):
|
|
8
|
+
#
|
|
9
|
+
# PaygatePk::EasyPaisa::MobileAccount.charge(...) # => Contracts::ChargeResult
|
|
10
|
+
# PaygatePk::EasyPaisa::OTC.create(...) # => Contracts::ChargeResult
|
|
11
|
+
# PaygatePk::EasyPaisa::Inquiry.fetch(...) # => Contracts::InquiryResult
|
|
12
|
+
#
|
|
13
|
+
# Callback (IPN) verification is deferred to 1.2 until Easypaisa
|
|
14
|
+
# publishes the full IPN wire spec; for now configure the IPN URL in
|
|
15
|
+
# the Easypaisa Merchant Portal and use Inquiry.fetch to poll status.
|
|
16
|
+
module EasyPaisa
|
|
17
|
+
def self.config
|
|
18
|
+
PaygatePk.config.easy_paisa
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -8,6 +8,10 @@ module PaygatePk
|
|
|
8
8
|
DEFAULT_FORM_ID = "paygate-pk-redirect-form"
|
|
9
9
|
SUBMIT_LABEL = "Pay now"
|
|
10
10
|
|
|
11
|
+
OTC_VOUCHER_SUCCESS_CLASS = "paygate-pk-otc-voucher rounded-2xl border border-emerald-200 bg-emerald-50 p-6"
|
|
12
|
+
OTC_VOUCHER_FAILURE_CLASS = "paygate-pk-otc-voucher paygate-pk-otc-voucher--failed " \
|
|
13
|
+
"rounded-2xl border border-rose-200 bg-rose-50 p-6"
|
|
14
|
+
|
|
11
15
|
# Renders the auto-submitting redirect form for any provider that
|
|
12
16
|
# produces a Contracts::RedirectRequest.
|
|
13
17
|
#
|
|
@@ -29,6 +33,26 @@ module PaygatePk
|
|
|
29
33
|
form_html + autosubmit_script(form_id)
|
|
30
34
|
end
|
|
31
35
|
|
|
36
|
+
# Renders a printable Easypaisa OTC voucher from a
|
|
37
|
+
# Contracts::ChargeResult (from PaygatePk::EasyPaisa::OTC.create).
|
|
38
|
+
#
|
|
39
|
+
# <%= paygate_pk_otc_voucher(@voucher) %>
|
|
40
|
+
#
|
|
41
|
+
# Options:
|
|
42
|
+
# html: { class:, ... } — outer container overrides
|
|
43
|
+
# title: heading text (default "Pay at any Easypaisa shop")
|
|
44
|
+
# instructions: array of bullet strings; falls back to a sane
|
|
45
|
+
# default that mentions the expiry time
|
|
46
|
+
def paygate_pk_otc_voucher(result, html: {}, title: "Pay at any Easypaisa shop", instructions: nil)
|
|
47
|
+
return otc_voucher_failure(result, html) unless result.success? && result.payment_token.present?
|
|
48
|
+
|
|
49
|
+
container_attrs = otc_voucher_attrs(html, success: true)
|
|
50
|
+
body = otc_voucher_success_body(result, title: title,
|
|
51
|
+
instructions: instructions || default_otc_instructions(result))
|
|
52
|
+
|
|
53
|
+
content_tag(:div, body, container_attrs)
|
|
54
|
+
end
|
|
55
|
+
|
|
32
56
|
private
|
|
33
57
|
|
|
34
58
|
def render_form(redirect, form_id, html, autosubmit, submit_label)
|
|
@@ -54,6 +78,82 @@ module PaygatePk
|
|
|
54
78
|
nonce: respond_to?(:content_security_policy_nonce) ? content_security_policy_nonce : nil
|
|
55
79
|
)
|
|
56
80
|
end
|
|
81
|
+
|
|
82
|
+
# ── OTC voucher rendering ─────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
def otc_voucher_attrs(html, success:)
|
|
85
|
+
default_class = success ? OTC_VOUCHER_SUCCESS_CLASS : OTC_VOUCHER_FAILURE_CLASS
|
|
86
|
+
html.merge(class: [default_class, html[:class]].compact.join(" "))
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def otc_voucher_success_body(result, title:, instructions:)
|
|
90
|
+
safe_join([
|
|
91
|
+
content_tag(:h3, title, class: "text-base font-bold text-slate-900 mb-4"),
|
|
92
|
+
otc_voucher_token_block(result),
|
|
93
|
+
otc_voucher_meta(result),
|
|
94
|
+
otc_voucher_instructions(instructions)
|
|
95
|
+
])
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def otc_voucher_token_block(result)
|
|
99
|
+
content_tag(:div, class: "rounded-xl bg-white border border-emerald-200 px-5 py-4 mb-4 text-center") do
|
|
100
|
+
safe_join([
|
|
101
|
+
content_tag(:p, "Payment token",
|
|
102
|
+
class: "text-xs font-semibold uppercase tracking-wide text-slate-500"),
|
|
103
|
+
content_tag(:p, result.payment_token,
|
|
104
|
+
class: "mt-1 font-mono text-2xl font-bold tracking-widest text-slate-900")
|
|
105
|
+
])
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def otc_voucher_meta(result)
|
|
110
|
+
rows = []
|
|
111
|
+
rows << ["Amount", "PKR #{result.amount}"]
|
|
112
|
+
rows << ["Mobile", result.customer[:msisdn]] if result.customer[:msisdn].present?
|
|
113
|
+
rows << ["Expires", otc_voucher_format_time(result.expires_at)] if result.expires_at
|
|
114
|
+
rows << ["Order", result.basket_id]
|
|
115
|
+
|
|
116
|
+
content_tag(:dl, class: "grid grid-cols-2 gap-y-2 text-sm mb-4") do
|
|
117
|
+
safe_join(rows.flat_map do |label, value|
|
|
118
|
+
[
|
|
119
|
+
content_tag(:dt, label, class: "text-slate-500"),
|
|
120
|
+
content_tag(:dd, value, class: "text-right font-semibold text-slate-900")
|
|
121
|
+
]
|
|
122
|
+
end)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def otc_voucher_instructions(instructions)
|
|
127
|
+
content_tag(:ul, class: "list-disc pl-5 space-y-1 text-xs text-slate-600") do
|
|
128
|
+
safe_join(instructions.map { |line| content_tag(:li, line) })
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def default_otc_instructions(result)
|
|
133
|
+
expiry = result.expires_at ? otc_voucher_format_time(result.expires_at) : nil
|
|
134
|
+
[
|
|
135
|
+
"Visit any Easypaisa shop with the token above.",
|
|
136
|
+
"Pay PKR #{result.amount} in cash and quote your token to the agent.",
|
|
137
|
+
expiry ? "Pay before #{expiry} — the token expires after that." : nil
|
|
138
|
+
].compact
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def otc_voucher_failure(result, html)
|
|
142
|
+
container_attrs = otc_voucher_attrs(html, success: false)
|
|
143
|
+
message = result.response_message.presence || "Easypaisa did not issue a token for this order."
|
|
144
|
+
body = safe_join([
|
|
145
|
+
content_tag(:h3, "Voucher unavailable", class: "text-base font-bold text-rose-900 mb-2"),
|
|
146
|
+
content_tag(:p, message, class: "text-sm text-rose-700")
|
|
147
|
+
])
|
|
148
|
+
content_tag(:div, body, container_attrs)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def otc_voucher_format_time(value)
|
|
152
|
+
return value if value.is_a?(String)
|
|
153
|
+
return nil unless value.respond_to?(:strftime)
|
|
154
|
+
|
|
155
|
+
value.strftime("%d %b %Y, %I:%M %p")
|
|
156
|
+
end
|
|
57
157
|
end
|
|
58
158
|
end
|
|
59
159
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
|
|
5
|
+
module PaygatePk
|
|
6
|
+
module Util
|
|
7
|
+
# Header-credential helpers used by REST providers that authenticate
|
|
8
|
+
# with a static username/password pair.
|
|
9
|
+
#
|
|
10
|
+
# Easypaisa: header key "Credentials", value is Base64Strict of
|
|
11
|
+
# "username:password" -- HTTP-Basic style but WITHOUT the literal
|
|
12
|
+
# "Basic " scheme prefix.
|
|
13
|
+
module Credentials
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# Returns Base64Strict("user:pass"). Raises if either is blank
|
|
17
|
+
# -- empty credentials would silently authenticate as a different
|
|
18
|
+
# principal and produce baffling 401s downstream.
|
|
19
|
+
def basic(username, password)
|
|
20
|
+
raise PaygatePk::ConfigurationError, "username is required" if Coercions.blank?(username)
|
|
21
|
+
raise PaygatePk::ConfigurationError, "password is required" if Coercions.blank?(password)
|
|
22
|
+
|
|
23
|
+
Base64.strict_encode64("#{username}:#{password}")
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/paygate_pk/version.rb
CHANGED
data/lib/paygate_pk.rb
CHANGED
|
@@ -7,12 +7,15 @@ require_relative "paygate_pk/config"
|
|
|
7
7
|
|
|
8
8
|
require_relative "paygate_pk/util/security"
|
|
9
9
|
require_relative "paygate_pk/util/signature/pay_fast"
|
|
10
|
+
require_relative "paygate_pk/util/credentials"
|
|
10
11
|
|
|
11
12
|
require_relative "paygate_pk/http/client"
|
|
12
13
|
|
|
13
14
|
require_relative "paygate_pk/contracts/access_token"
|
|
14
15
|
require_relative "paygate_pk/contracts/redirect_request"
|
|
15
16
|
require_relative "paygate_pk/contracts/callback_event"
|
|
17
|
+
require_relative "paygate_pk/contracts/charge_result"
|
|
18
|
+
require_relative "paygate_pk/contracts/inquiry_result"
|
|
16
19
|
|
|
17
20
|
require_relative "paygate_pk/pay_fast"
|
|
18
21
|
require_relative "paygate_pk/pay_fast/endpoints"
|
|
@@ -20,19 +23,36 @@ require_relative "paygate_pk/pay_fast/auth"
|
|
|
20
23
|
require_relative "paygate_pk/pay_fast/redirect"
|
|
21
24
|
require_relative "paygate_pk/pay_fast/callback"
|
|
22
25
|
|
|
26
|
+
require_relative "paygate_pk/easy_paisa"
|
|
27
|
+
require_relative "paygate_pk/easy_paisa/endpoints"
|
|
28
|
+
require_relative "paygate_pk/easy_paisa/client"
|
|
29
|
+
require_relative "paygate_pk/easy_paisa/mobile_account"
|
|
30
|
+
require_relative "paygate_pk/easy_paisa/otc"
|
|
31
|
+
require_relative "paygate_pk/easy_paisa/inquiry"
|
|
32
|
+
|
|
23
33
|
require_relative "paygate_pk/rails/railtie" if defined?(Rails::Railtie)
|
|
24
34
|
|
|
25
35
|
# Unified Ruby/Rails client for Pakistani payment gateways.
|
|
26
36
|
#
|
|
27
37
|
# PaygatePk.configure do |c|
|
|
38
|
+
# # PayFast (hosted checkout / redirection)
|
|
28
39
|
# c.pay_fast.environment = :sandbox
|
|
29
40
|
# c.pay_fast.merchant_id = ENV["PAYFAST_MERCHANT_ID"]
|
|
30
41
|
# c.pay_fast.secured_key = ENV["PAYFAST_SECURED_KEY"]
|
|
31
42
|
# c.pay_fast.merchant_name = "Acme Store"
|
|
43
|
+
#
|
|
44
|
+
# # Easypaisa (REST APIs)
|
|
45
|
+
# c.easy_paisa.environment = :sandbox
|
|
46
|
+
# c.easy_paisa.username = ENV["EASYPAISA_USERNAME"]
|
|
47
|
+
# c.easy_paisa.password = ENV["EASYPAISA_PASSWORD"]
|
|
48
|
+
# c.easy_paisa.store_id = ENV["EASYPAISA_STORE_ID"]
|
|
32
49
|
# end
|
|
33
50
|
#
|
|
34
51
|
# redirect = PaygatePk::PayFast::Redirect.build(...)
|
|
35
52
|
# event = PaygatePk::PayFast::Callback.verify!(request.parameters)
|
|
53
|
+
# result = PaygatePk::EasyPaisa::MobileAccount.charge(...)
|
|
54
|
+
# voucher = PaygatePk::EasyPaisa::OTC.create(...)
|
|
55
|
+
# status = PaygatePk::EasyPaisa::Inquiry.fetch(...)
|
|
36
56
|
module PaygatePk
|
|
37
57
|
class << self
|
|
38
58
|
def configure
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: paygate_pk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Talha Junaid
|
|
@@ -173,7 +173,15 @@ files:
|
|
|
173
173
|
- lib/paygate_pk/config.rb
|
|
174
174
|
- lib/paygate_pk/contracts/access_token.rb
|
|
175
175
|
- lib/paygate_pk/contracts/callback_event.rb
|
|
176
|
+
- lib/paygate_pk/contracts/charge_result.rb
|
|
177
|
+
- lib/paygate_pk/contracts/inquiry_result.rb
|
|
176
178
|
- lib/paygate_pk/contracts/redirect_request.rb
|
|
179
|
+
- lib/paygate_pk/easy_paisa.rb
|
|
180
|
+
- lib/paygate_pk/easy_paisa/client.rb
|
|
181
|
+
- lib/paygate_pk/easy_paisa/endpoints.rb
|
|
182
|
+
- lib/paygate_pk/easy_paisa/inquiry.rb
|
|
183
|
+
- lib/paygate_pk/easy_paisa/mobile_account.rb
|
|
184
|
+
- lib/paygate_pk/easy_paisa/otc.rb
|
|
177
185
|
- lib/paygate_pk/errors.rb
|
|
178
186
|
- lib/paygate_pk/http/client.rb
|
|
179
187
|
- lib/paygate_pk/pay_fast.rb
|
|
@@ -183,6 +191,7 @@ files:
|
|
|
183
191
|
- lib/paygate_pk/pay_fast/redirect.rb
|
|
184
192
|
- lib/paygate_pk/rails/railtie.rb
|
|
185
193
|
- lib/paygate_pk/rails/view_helpers.rb
|
|
194
|
+
- lib/paygate_pk/util/credentials.rb
|
|
186
195
|
- lib/paygate_pk/util/security.rb
|
|
187
196
|
- lib/paygate_pk/util/signature/pay_fast.rb
|
|
188
197
|
- lib/paygate_pk/version.rb
|