przelewy24_payment 0.2.0 → 2.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 +5 -5
- data/CHANGELOG.md +11 -0
- data/LICENSE.txt +17 -18
- data/README.md +345 -139
- data/app/controllers/przelewy24_payment/webhooks_controller.rb +63 -0
- data/app/jobs/przelewy24_payment/application_job.rb +6 -0
- data/app/jobs/przelewy24_payment/charge_due_subscriptions_job.rb +13 -0
- data/app/models/przelewy24_payment/application_record.rb +7 -0
- data/app/models/przelewy24_payment/payment.rb +177 -0
- data/app/models/przelewy24_payment/refund.rb +33 -0
- data/app/models/przelewy24_payment/subscription.rb +147 -0
- data/config/routes.rb +6 -0
- data/lib/generators/przelewy24_payment/install/install_generator.rb +60 -5
- data/lib/generators/przelewy24_payment/install/templates/README +14 -0
- data/lib/generators/przelewy24_payment/install/templates/create_przelewy24_payments.rb.tt +21 -0
- data/lib/generators/przelewy24_payment/install/templates/create_przelewy24_refunds.rb.tt +14 -0
- data/lib/generators/przelewy24_payment/install/templates/create_przelewy24_subscriptions.rb.tt +28 -0
- data/lib/generators/przelewy24_payment/install/templates/initializer.rb.tt +47 -0
- data/lib/przelewy24_payment/api/base.rb +17 -0
- data/lib/przelewy24_payment/api/blik.rb +41 -0
- data/lib/przelewy24_payment/api/cards.rb +26 -0
- data/lib/przelewy24_payment/api/payment_methods.rb +11 -0
- data/lib/przelewy24_payment/api/refunds.rb +25 -0
- data/lib/przelewy24_payment/api/transactions.rb +51 -0
- data/lib/przelewy24_payment/client.rb +66 -0
- data/lib/przelewy24_payment/configuration.rb +81 -0
- data/lib/przelewy24_payment/engine.rb +12 -1
- data/lib/przelewy24_payment/errors.rb +45 -0
- data/lib/przelewy24_payment/instrumentation.rb +16 -0
- data/lib/przelewy24_payment/money.rb +26 -0
- data/lib/przelewy24_payment/notification.rb +55 -0
- data/lib/przelewy24_payment/payable.rb +20 -0
- data/lib/przelewy24_payment/refund_notification.rb +44 -0
- data/lib/przelewy24_payment/signature.rb +64 -0
- data/lib/przelewy24_payment/test_helpers.rb +20 -0
- data/lib/przelewy24_payment/version.rb +3 -1
- data/lib/przelewy24_payment.rb +26 -187
- metadata +83 -24
- data/.gitignore +0 -1
- data/Gemfile +0 -4
- data/Rakefile +0 -2
- data/app/helpers/przelewy24_payment/payment_helper.rb +0 -5
- data/app/views/przelewy24_payment/_payment_form.html.haml +0 -28
- data/lib/generators/przelewy24_payment/install/USAGE +0 -8
- data/lib/generators/przelewy24_payment/install/templates/przelewy24_payment.rb +0 -16
- data/lib/przelewy24_payment/przelewy24_payment_controller.rb +0 -67
- data/lib/tasks/test_connection.rake +0 -18
- data/przelewy24_payment.gemspec +0 -19
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: e190d58b341f9dcaf800cd181bddb65b06f6dd6841a18d64575441bf8a2d5d9e
|
|
4
|
+
data.tar.gz: b18a7a62dc2304162bee55c61a160bd03e87e55c47b1178c3ed85c692876031f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ac2b35aeebecbdce89739b5fcccf5a4ba1a3e74e8d1ddb8e5944e96d9a61509c4eacda3e73326e3a0bab91b974c11adcd84a45a71495f1577fbef65144054b81
|
|
7
|
+
data.tar.gz: c46bdc37bd358135020b83ffe67ab8caa29014bf7e4d6adeb0f435801e88a31e149de2359d82631d8321a074e7abed07822b34f5ec6ef3bea2a0da0ee5115484
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 2.0.0
|
|
4
|
+
|
|
5
|
+
Complete rewrite as a Rails engine.
|
|
6
|
+
|
|
7
|
+
- REST API v1 client (Faraday): `testAccess`, register, verify, by-session lookup, refunds, payment methods.
|
|
8
|
+
- `Payment` / `Refund` models, `Payable` concern, idempotent signed webhooks.
|
|
9
|
+
- Install generator, credentials/ENV configuration, ActiveSupport::Notifications events.
|
|
10
|
+
- BLIK (code, 1-click, aliases), card reference charges, `Subscription` model with scheduled charges, retries and cancellation.
|
|
11
|
+
- TLS verification always on; money handled as integer minor units.
|
data/LICENSE.txt
CHANGED
|
@@ -1,22 +1,21 @@
|
|
|
1
|
-
Copyright (c) 2012 TODO: Write your name
|
|
2
|
-
|
|
3
1
|
MIT License
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
the
|
|
3
|
+
Copyright (c) 2026 Jakub Cieślar
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
12
11
|
|
|
13
|
-
The above copyright notice and this permission notice shall be
|
|
14
|
-
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
15
14
|
|
|
16
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
OF
|
|
22
|
-
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
CHANGED
|
@@ -1,178 +1,384 @@
|
|
|
1
|
-
#
|
|
1
|
+
# przelewy24_payment
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Rails engine for [Przelewy24](https://www.przelewy24.pl) (REST API v1): transaction registration, signed webhook handling with automatic verification, refunds, payment methods, an install generator and test helpers.
|
|
4
|
+
|
|
5
|
+
Requires Rails 7.1+ and Ruby 3.2+.
|
|
4
6
|
|
|
5
7
|
## Installation
|
|
6
8
|
|
|
7
|
-
Add
|
|
9
|
+
Add the gem to your `Gemfile` and run the install generator:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bundle add przelewy24_payment
|
|
13
|
+
bin/rails generate przelewy24_payment:install
|
|
14
|
+
bin/rails db:migrate
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### What the generator does
|
|
18
|
+
|
|
19
|
+
`rails generate przelewy24_payment:install` (alias: `rails g przelewy24_payment:install`) performs four steps:
|
|
20
|
+
|
|
21
|
+
| Step | Result |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| Initializer | `config/initializers/przelewy24_payment.rb` with all settings and callbacks |
|
|
24
|
+
| Migrations | `create_przelewy24_payments`, `create_przelewy24_refunds` and `create_przelewy24_subscriptions` in `db/migrate` |
|
|
25
|
+
| Route | `mount Przelewy24Payment::Engine => "/przelewy24"` in `config/routes.rb` |
|
|
26
|
+
| Next steps | a short checklist printed to the terminal |
|
|
27
|
+
|
|
28
|
+
Options:
|
|
29
|
+
|
|
30
|
+
| Option | Default | Purpose |
|
|
31
|
+
| --- | --- | --- |
|
|
32
|
+
| `--mount-path=/payments/p24` | `/przelewy24` | where the webhook engine is mounted; the `urlStatus` sent to Przelewy24 follows it automatically |
|
|
33
|
+
| `--skip-subscriptions` | off | do not generate the subscriptions migration if you only need one-off payments |
|
|
8
34
|
|
|
9
|
-
|
|
35
|
+
The command is safe to re-run: the route is added only once and existing files prompt before being overwritten (use `--force` to overwrite, `--skip` to keep yours). Use `rails destroy przelewy24_payment:install` to remove the generated files and the route.
|
|
10
36
|
|
|
11
|
-
|
|
37
|
+
Then:
|
|
12
38
|
|
|
13
|
-
|
|
39
|
+
1. Add your credentials (see below).
|
|
40
|
+
2. Include `Przelewy24Payment::Payable` in the model that gets paid (e.g. `Order`).
|
|
41
|
+
3. Make your app reachable by Przelewy24: set `config.url_options` to your public host. In development use a tunnel (ngrok, cloudflared) because Przelewy24 must be able to reach `POST /przelewy24/webhooks/status`.
|
|
14
42
|
|
|
15
|
-
|
|
43
|
+
The engine works with any model: the payment is linked polymorphically, so no columns are added to your own tables.
|
|
16
44
|
|
|
17
|
-
|
|
45
|
+
## Configuration
|
|
18
46
|
|
|
19
|
-
|
|
47
|
+
All settings live in `config/initializers/przelewy24_payment.rb`. Values passed there win over ENV.
|
|
20
48
|
|
|
21
|
-
|
|
49
|
+
### Customizing
|
|
22
50
|
|
|
23
|
-
|
|
51
|
+
**Mount path.** Use `rails g przelewy24_payment:install --mount-path=/payments/p24`, or change the `mount` line in `config/routes.rb` yourself.
|
|
52
|
+
|
|
53
|
+
**Webhook controller.** Set `config.parent_controller = "ApplicationController"` (or any controller of yours) to reuse your logging, error reporting or `rescue_from` handlers. CSRF protection is skipped automatically. Do not inherit from a controller that requires a signed-in user: Przelewy24 calls the endpoints without a session.
|
|
54
|
+
|
|
55
|
+
**Models.** The models live under `Przelewy24Payment::` with `przelewy24_*` tables, so they never clash with your own `Payment` or `Order`. Extend them with load hooks instead of forking:
|
|
24
56
|
|
|
25
57
|
```ruby
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
config.language = 'pl'
|
|
31
|
-
config.currency = 'PLN'
|
|
32
|
-
config.country = 'PL'
|
|
33
|
-
config.mode = :development
|
|
34
|
-
config.url_status = '/your_controller/comeback'
|
|
35
|
-
config.url_return = '/your_controller/comeback'
|
|
36
|
-
config.hostname = {
|
|
37
|
-
:development => "http://127.0.0.1:3000",
|
|
38
|
-
:production => "your.domain",
|
|
39
|
-
:staging => "staging.domain"
|
|
40
|
-
}
|
|
41
|
-
end
|
|
58
|
+
# config/initializers/przelewy24_payment.rb
|
|
59
|
+
ActiveSupport.on_load(:przelewy24_payment_payment) do # also :przelewy24_payment_refund, :przelewy24_payment_subscription
|
|
60
|
+
scope :recent, -> { where(created_at: 1.week.ago..) }
|
|
61
|
+
after_commit :track_in_analytics, on: :update
|
|
42
62
|
|
|
63
|
+
def track_in_analytics = Analytics.track("p24_#{status}", id: id)
|
|
64
|
+
end
|
|
43
65
|
```
|
|
44
66
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
67
|
+
The payable side is yours entirely: any model can `include Przelewy24Payment::Payable`, whatever its name or table.
|
|
68
|
+
|
|
69
|
+
### Credentials
|
|
70
|
+
|
|
71
|
+
Rails credentials (`bin/rails credentials:edit`) are read first by the generated initializer:
|
|
72
|
+
|
|
73
|
+
```yaml
|
|
74
|
+
przelewy24:
|
|
75
|
+
merchant_id: 12345
|
|
76
|
+
pos_id: 12345 # optional, defaults to merchant_id
|
|
77
|
+
api_key: "..." # REST API key ("klucz do raportów") from the Przelewy24 panel
|
|
78
|
+
crc_key: "..." # CRC key of the shop
|
|
48
79
|
```
|
|
49
|
-
## Usage
|
|
50
80
|
|
|
51
|
-
|
|
81
|
+
ENV variables are the fallback, and are also read directly if the initializer does not set the value:
|
|
82
|
+
|
|
83
|
+
| Variable | Required | Purpose |
|
|
84
|
+
| --- | --- | --- |
|
|
85
|
+
| `PRZELEWY24_MERCHANT_ID` | yes | merchant id |
|
|
86
|
+
| `PRZELEWY24_POS_ID` | no | shop id, defaults to the merchant id |
|
|
87
|
+
| `PRZELEWY24_API_KEY` | yes | REST API key (HTTP basic auth password) |
|
|
88
|
+
| `PRZELEWY24_CRC_KEY` | yes | key used to sign requests and verify webhooks |
|
|
89
|
+
| `PRZELEWY24_ENVIRONMENT` | no | `sandbox` (default) or `production` |
|
|
90
|
+
|
|
91
|
+
Missing credentials raise `Przelewy24Payment::ConfigurationError` on the first API call (not at boot), naming every missing key. Sandbox and production use different credentials, so keep them per environment.
|
|
92
|
+
|
|
93
|
+
### Options
|
|
94
|
+
|
|
95
|
+
| Option | Default | Purpose |
|
|
96
|
+
| --- | --- | --- |
|
|
97
|
+
| `environment` | `:sandbox` | `:sandbox` or `:production` |
|
|
98
|
+
| `default_country` / `default_language` | `"PL"` / `"pl"` | sent on registration |
|
|
99
|
+
| `url_options` | `{}` | host and protocol used to build the status webhook URL, e.g. `{ host: "shop.example.com", protocol: "https" }` |
|
|
100
|
+
| `allowed_ips` | `nil` | restrict webhooks to these IPs or CIDR ranges (Przelewy24 publishes them in its docs) |
|
|
101
|
+
| `open_timeout` / `timeout` | `5` / `15` | seconds |
|
|
102
|
+
| `logger` | `Rails.logger` | |
|
|
103
|
+
| `on_payment_verified`, `on_payment_failed`, `on_refund` | `nil` | callbacks, see [Webhooks](#webhooks) |
|
|
104
|
+
| `parent_controller` | `"ActionController::API"` | class the webhook controller inherits from, see below |
|
|
105
|
+
| `max_failed_attempts` | `3` | failed subscription charges before it is canceled |
|
|
106
|
+
| `retry_interval` | `1.day` | wait before retrying a failed subscription charge |
|
|
107
|
+
|
|
108
|
+
## Usage
|
|
52
109
|
|
|
53
110
|
```ruby
|
|
54
|
-
class
|
|
55
|
-
include
|
|
56
|
-
|
|
111
|
+
class Order < ApplicationRecord
|
|
112
|
+
include Przelewy24Payment::Payable # has_many :przelewy24_payments
|
|
113
|
+
|
|
114
|
+
def payment_verified! = update!(status: :paid)
|
|
115
|
+
def payment_failed! = update!(status: :payment_failed)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Controller: every attempt creates a new payment with a fresh session id.
|
|
119
|
+
payment = Przelewy24Payment::Payment.start!(
|
|
120
|
+
payable: order, amount: order.price, currency: order.currency,
|
|
121
|
+
email: current_user.email, description: "Order #{order.id}",
|
|
122
|
+
return_url: order_url(order)
|
|
123
|
+
)
|
|
124
|
+
redirect_to payment.redirect_url, allow_other_host: true
|
|
57
125
|
```
|
|
58
126
|
|
|
59
|
-
|
|
127
|
+
Amounts are decimals in major units (`BigDecimal`, `String` or `Integer`); the gem converts to grosze.
|
|
128
|
+
|
|
129
|
+
### Webhooks
|
|
130
|
+
|
|
131
|
+
The engine ships the webhook endpoints, so your app needs no controller or route of its own. After `rails g przelewy24_payment:install` the engine is mounted at `/przelewy24` and exposes:
|
|
132
|
+
|
|
133
|
+
| Endpoint | Purpose |
|
|
134
|
+
| --- | --- |
|
|
135
|
+
| `POST /przelewy24/webhooks/status` | transaction status notification |
|
|
136
|
+
| `POST /przelewy24/webhooks/refund` | refund status notification |
|
|
137
|
+
|
|
138
|
+
The status URL is sent to Przelewy24 automatically as `urlStatus` when you call `Payment.start!`. It is built from `config.url_options`, so in development point that at a tunnel (ngrok, cloudflared). You do not need to enter anything in the Przelewy24 panel.
|
|
139
|
+
|
|
140
|
+
#### What happens when a notification arrives
|
|
141
|
+
|
|
142
|
+
1. The JSON body is parsed and its `sign` (SHA-384) and `merchantId` are verified. Optionally the request IP is checked against `allowed_ips`.
|
|
143
|
+
2. The `Payment` row is looked up by `sessionId` and locked.
|
|
144
|
+
3. The transaction is confirmed with `PUT /transaction/verify`, which Przelewy24 requires for settlement. The payment becomes `verified` or `failed`. This happens only once per payment, so repeated notifications never trigger a second verification.
|
|
145
|
+
4. Your callback runs (`on_payment_verified` or `on_payment_failed`), and the payment is stamped with `notified_at` in the same database transaction.
|
|
146
|
+
|
|
147
|
+
#### Hooking into webhooks from your app
|
|
148
|
+
|
|
149
|
+
Set callbacks in `config/initializers/przelewy24_payment.rb`. They receive the `Przelewy24Payment::Payment`; `payment.payable` is your model (e.g. the `Order`).
|
|
60
150
|
|
|
61
151
|
```ruby
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
152
|
+
Przelewy24Payment.configure do |config|
|
|
153
|
+
config.on_payment_verified = ->(payment) { payment.payable.mark_as_paid! }
|
|
154
|
+
config.on_payment_failed = ->(payment) { payment.payable.mark_as_failed! }
|
|
155
|
+
config.on_refund = ->(refund) { RefundMailer.completed(refund).deliver_later }
|
|
156
|
+
end
|
|
157
|
+
```
|
|
65
158
|
|
|
66
|
-
|
|
67
|
-
# so you can do whatever you want
|
|
68
|
-
def payment_success(payment_params)
|
|
69
|
-
# payment_params returns hash with:
|
|
70
|
-
# p24_merchant_id
|
|
71
|
-
# p24_pos_id
|
|
72
|
-
# p24_session_id
|
|
73
|
-
# p24_order_id
|
|
74
|
-
# p24_amount
|
|
75
|
-
# p24_currency
|
|
76
|
-
# p24_method
|
|
77
|
-
# p24_sign
|
|
78
|
-
# p24_karta
|
|
79
|
-
# payment_id
|
|
159
|
+
For side effects that should not live in the configuration (metrics, mails, auditing), subscribe to the instrumentation events instead, see [Instrumentation](#instrumentation).
|
|
80
160
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
161
|
+
#### Delivery guarantee: at least once
|
|
162
|
+
|
|
163
|
+
The callback and the `notified_at` stamp share one transaction. If the callback raises:
|
|
164
|
+
|
|
165
|
+
- the transaction is rolled back, including any database changes the callback made;
|
|
166
|
+
- the endpoint answers `500`, so Przelewy24 retries the notification;
|
|
167
|
+
- the retry does not verify the transaction again, it only runs the callback again.
|
|
168
|
+
|
|
169
|
+
A paid order can therefore never be left un-notified because of a transient error in your code. The flip side is that a callback may run more than once, so keep side effects outside the database (emails, HTTP calls, enqueued jobs) idempotent. `payment.notified_at` tells you whether the callback has completed.
|
|
170
|
+
|
|
171
|
+
#### Responses
|
|
172
|
+
|
|
173
|
+
| Status | Meaning |
|
|
174
|
+
| --- | --- |
|
|
175
|
+
| `200` | processed (also returned for duplicate notifications) |
|
|
176
|
+
| `400` | malformed or incomplete payload |
|
|
177
|
+
| `401` | invalid signature |
|
|
178
|
+
| `403` | IP not in `allowed_ips` |
|
|
179
|
+
| `404` | unknown `sessionId` |
|
|
180
|
+
| `500` | Przelewy24 API unavailable or your callback raised; Przelewy24 will retry |
|
|
84
181
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
182
|
+
### Failed, abandoned and retried payments
|
|
183
|
+
|
|
184
|
+
Every payment attempt is its own `Przelewy24Payment::Payment` with a fresh `session_id`, so retrying is simply starting a new payment for the same record. A payment is in one of these states:
|
|
185
|
+
|
|
186
|
+
| Status | Meaning |
|
|
187
|
+
| --- | --- |
|
|
188
|
+
| `pending` | registered, waiting for the customer or for the notification |
|
|
189
|
+
| `verified` | confirmed by Przelewy24, money received |
|
|
190
|
+
| `failed` | Przelewy24 rejected the verification |
|
|
191
|
+
| `expired` | abandoned attempt closed by `Payment.expire_stale!` |
|
|
192
|
+
|
|
193
|
+
**The customer cancels or closes the gateway.** Przelewy24 sends no notification in this case, the customer just comes back to your `return_url`. The payment stays `pending`. Do not mark anything as paid or failed on the return page; show the current state and offer a retry:
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
class OrdersController < ApplicationController
|
|
197
|
+
def pay
|
|
198
|
+
order = current_user.orders.find(params[:id])
|
|
199
|
+
return redirect_to(order, notice: "Already paid.") if order.przelewy24_paid?
|
|
200
|
+
|
|
201
|
+
payment = Przelewy24Payment::Payment.start!(
|
|
202
|
+
payable: order, amount: order.price, currency: order.currency,
|
|
203
|
+
email: current_user.email, description: "Order #{order.id}", return_url: order_url(order)
|
|
204
|
+
)
|
|
205
|
+
redirect_to payment.redirect_url, allow_other_host: true
|
|
206
|
+
rescue Przelewy24Payment::AlreadyPaidError
|
|
207
|
+
redirect_to order, notice: "Already paid."
|
|
208
|
+
rescue Przelewy24Payment::Error => e
|
|
209
|
+
redirect_to order, alert: "Payment could not be started: #{e.message}"
|
|
102
210
|
end
|
|
103
211
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
# e.g:
|
|
108
|
-
# you must return hash with amount which was save in your db and your crc_key
|
|
109
|
-
payment = Payment::Payment.where(session_id: response_params['p24_session_id']).first
|
|
110
|
-
if payment
|
|
111
|
-
{ amount: payment.amount, crc_key: Przelewy24Payment.crc_key }
|
|
112
|
-
else
|
|
113
|
-
{}
|
|
114
|
-
end
|
|
212
|
+
def show
|
|
213
|
+
@order = current_user.orders.find(params[:id])
|
|
214
|
+
@payment = @order.latest_przelewy24_payment # pending / verified / failed
|
|
115
215
|
end
|
|
216
|
+
end
|
|
116
217
|
```
|
|
117
218
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
```ruby
|
|
121
|
-
= payment_button(@data)
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
And also on your payment controller you should specify @data hash e.g:
|
|
125
|
-
|
|
126
|
-
```ruby
|
|
127
|
-
class YourPaymentController < ApplicationController
|
|
128
|
-
include Przelewy24PaymentController
|
|
129
|
-
...
|
|
130
|
-
|
|
131
|
-
def your_payment
|
|
132
|
-
session_id = Przelewy24Payment.friendly_token[0,20] # assign this to payment
|
|
133
|
-
@data = { :session_id => session_id,
|
|
134
|
-
:description => "opis",
|
|
135
|
-
:amount => 1.23,
|
|
136
|
-
:email => 'payment@example.com',
|
|
137
|
-
:country => 'PL',
|
|
138
|
-
# adding this params, you overwrite your config settings so this param is optional
|
|
139
|
-
# :merchant_id => merchant_id
|
|
140
|
-
# :pos_id => pos_id
|
|
141
|
-
# :api_version => api_version,
|
|
142
|
-
# :crc_key => crc_key,
|
|
143
|
-
# :currency => currency,
|
|
144
|
-
# :country => country,
|
|
145
|
-
# :url_return => url_return,
|
|
146
|
-
# :url_status => url_status,
|
|
147
|
-
|
|
148
|
-
# other optional params
|
|
149
|
-
# :language => pl/en/de/es/it
|
|
150
|
-
# :method => method,
|
|
151
|
-
# :client => 'Adam Nowak',
|
|
152
|
-
# :address => 'Powstancow 22/2',
|
|
153
|
-
# :zipcode => '53-456',
|
|
154
|
-
# :city => 'Wroclaw',
|
|
155
|
-
# :phone => '481321132123',
|
|
156
|
-
# :time_limit => INT,
|
|
157
|
-
# :wait_for_result => INT,
|
|
158
|
-
# :channel => INT,
|
|
159
|
-
# :shipping => INT,
|
|
160
|
-
# :transfer_label => STRING(20)
|
|
161
|
-
# :encoding => ISO-8859-2/UTF-8/Windows-1250
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
end
|
|
219
|
+
Call `pay` again to retry: it creates a new attempt. Show a "Pay again" button whenever the order is not paid.
|
|
165
220
|
|
|
166
|
-
|
|
221
|
+
**Double-payment protection.** `Payment.start!` raises `Przelewy24Payment::AlreadyPaidError` when the payable already has a verified payment, so a retry can never charge the customer twice. An older attempt can still be paid late (for example from a tab that stayed open); its notification is processed normally. Make `on_payment_verified` idempotent for that case, and refund the extra payment if it happens:
|
|
167
222
|
|
|
223
|
+
```ruby
|
|
224
|
+
config.on_payment_verified = lambda do |payment|
|
|
225
|
+
order = payment.payable
|
|
226
|
+
order.paid? ? Rails.logger.warn("Duplicate payment #{payment.id}") : order.payment_verified!
|
|
227
|
+
end
|
|
168
228
|
```
|
|
169
229
|
|
|
170
|
-
|
|
230
|
+
**Verification rejected.** If Przelewy24 rejects the verification, the payment becomes `failed` and `on_payment_failed` runs. The customer can retry the same way.
|
|
231
|
+
|
|
232
|
+
**Registration errors.** If Przelewy24 refuses the registration or is unreachable, `Payment.start!` raises (`ValidationError`, `AuthenticationError`, `ConnectionError`, ...) and the attempt is marked `failed`. Nothing is left pending.
|
|
233
|
+
|
|
234
|
+
**Cleaning up abandoned attempts.** Run `Payment.expire_stale!` from a recurring job (cron, Solid Queue, GoodJob) to close attempts that never got a notification. Expired payments are still settled if a late notification arrives.
|
|
235
|
+
|
|
236
|
+
```ruby
|
|
237
|
+
class ExpirePrzelewy24PaymentsJob < ApplicationJob
|
|
238
|
+
def perform = Przelewy24Payment::Payment.expire_stale!(older_than: 1.day)
|
|
239
|
+
end
|
|
240
|
+
```
|
|
171
241
|
|
|
172
|
-
|
|
242
|
+
### BLIK on your own page
|
|
243
|
+
|
|
244
|
+
Instead of redirecting to the Przelewy24 gateway, the customer can type the 6-digit BLIK code in your checkout. Register the transaction with the customer's `PSU` (IP and user agent, required by Przelewy24), then charge it:
|
|
245
|
+
|
|
246
|
+
```ruby
|
|
247
|
+
payment = Przelewy24Payment::Payment.start!(
|
|
248
|
+
payable: order, amount: order.price, currency: "PLN", email: user.email,
|
|
249
|
+
description: "Order #{order.id}", return_url: order_url(order),
|
|
250
|
+
additional: { PSU: { IP: request.remote_ip, userAgent: request.user_agent } }
|
|
251
|
+
)
|
|
252
|
+
payment.charge_blik_code!(blik_code: params[:blik_code])
|
|
253
|
+
# The result still arrives through the status webhook; show "confirm in your banking app".
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Save an alias for later 1-click payments by passing `alias_value:` and `alias_label:` (5-35 characters) to `charge_blik_code!`. Later payments by that alias need no code:
|
|
257
|
+
|
|
258
|
+
```ruby
|
|
259
|
+
client = Przelewy24Payment.client
|
|
260
|
+
client.blik.aliases(user.email) # stored aliases (up to ~60 s after a payment)
|
|
261
|
+
|
|
262
|
+
payment = Przelewy24Payment::Payment.start!(..., method_ref_id: alias_value, additional: { PSU: psu })
|
|
263
|
+
payment.charge_blik_alias!(alias_value: alias_value)
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
If the alias is linked to several bank apps, Przelewy24 answers `409` and the gem raises `Przelewy24Payment::AmbiguousAliasError`; let the customer pick one of `error.alternative_keys` and call `client.blik.charge_by_alternative_key(token:, alternative_key:)`. An expired alias (error 68) needs a new code payment.
|
|
267
|
+
|
|
268
|
+
Sandbox: use the code `777XXX` for a successful BLIK payment and any other six digits to simulate a failure.
|
|
269
|
+
|
|
270
|
+
### Cards (one-click and recurring)
|
|
271
|
+
|
|
272
|
+
Card references are not stored by the gem. After a paid card transaction fetch the reference and keep it for later charges:
|
|
273
|
+
|
|
274
|
+
```ruby
|
|
275
|
+
ref = Przelewy24Payment.client.cards.reference_for(payment.p24_order_id) # GET card/info/{orderId}
|
|
276
|
+
|
|
277
|
+
payment = Przelewy24Payment::Payment.start!(..., method_ref_id: ref)
|
|
278
|
+
payment.charge_card! # off-session, no customer present
|
|
279
|
+
payment.charge_card!(three_ds: true) # => { order_id:, redirect_url: } redirect the customer when present
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
References from Google Pay, Apple Pay or VISA Mobile cannot be used for recurring charges. Card one-click and recurring features must be enabled for your account by Przelewy24.
|
|
283
|
+
|
|
284
|
+
### Subscriptions
|
|
285
|
+
|
|
286
|
+
Przelewy24 has **no subscription object and no cancel endpoint**. It only lets you charge a stored BLIK alias or card reference without the customer present. `Przelewy24Payment::Subscription` keeps the schedule on your side and drives those charges:
|
|
287
|
+
|
|
288
|
+
| Status | Meaning |
|
|
289
|
+
| --- | --- |
|
|
290
|
+
| `pending` | first payment not confirmed yet |
|
|
291
|
+
| `active` | charged every `frequency` while payments succeed |
|
|
292
|
+
| `past_due` | the last charge failed; retried after `retry_interval` |
|
|
293
|
+
| `canceled` | no more charges (by you, by the customer, or after `max_failed_attempts`) |
|
|
294
|
+
|
|
295
|
+
**1. Start (customer present).** `frequency` uses Przelewy24 notation: a number and `D`, `W`, `M` or `Y` (`"1M"`, `"7D"`).
|
|
296
|
+
|
|
297
|
+
```ruby
|
|
298
|
+
subscription = Przelewy24Payment::Subscription.start!(
|
|
299
|
+
owner: current_user, payment_method: :card, # or :blik
|
|
300
|
+
amount: BigDecimal("29.00"), currency: "PLN", email: current_user.email,
|
|
301
|
+
description: "Pro plan", frequency: "1M", return_url: account_url,
|
|
302
|
+
psu: { IP: request.remote_ip, userAgent: request.user_agent }
|
|
303
|
+
)
|
|
304
|
+
redirect_to subscription.first_payment.redirect_url, allow_other_host: true
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
For BLIK pass `recurring:` exactly as described in the Przelewy24 docs (type `O`, `M` or `A`, frequency, limits), then complete the first payment with `subscription.first_payment.charge_blik_code!(blik_code: params[:code])`. The alias and `recurring` stored on the subscription are used automatically. BLIK recurring payments are **not enabled by default**; ask Przelewy24 support to switch them on.
|
|
308
|
+
|
|
309
|
+
**2. Activation.** When the first payment is verified by the status webhook, the subscription becomes `active`; the card reference (via `card/info`) or the BLIK alias is stored and `next_charge_at` is set.
|
|
310
|
+
|
|
311
|
+
**3. Renewals.** Schedule the job (hourly is fine) with cron, Solid Queue recurring tasks or GoodJob cron:
|
|
312
|
+
|
|
313
|
+
```ruby
|
|
314
|
+
Przelewy24Payment::ChargeDueSubscriptionsJob.perform_later # charges due subscriptions and closes stale attempts
|
|
315
|
+
# or: Przelewy24Payment::Subscription.charge_due!
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Each charge creates a normal `Payment` (its `payable` is the subscription), registers it with the stored method and waits for the status webhook, which renews the subscription. A charge already in flight is never duplicated.
|
|
319
|
+
|
|
320
|
+
**4. Failures.** A rejected, failed or expired charge moves the subscription to `past_due` and retries after `retry_interval`. After `max_failed_attempts` consecutive failures it is canceled with the last error as `cancel_reason`. A failed first payment cancels the pending subscription.
|
|
321
|
+
|
|
322
|
+
**5. Cancel.** `subscription.cancel!(reason: "customer_request")` stops all future charges. Przelewy24 is not contacted (there is nothing to cancel there). If the customer revokes consent in their bank app, the next charge fails and the retry rules above apply.
|
|
323
|
+
|
|
324
|
+
React to lifecycle changes with instrumentation events (`subscription_activated`, `subscription_renewed`, `subscription_past_due`, `subscription_canceled`):
|
|
325
|
+
|
|
326
|
+
```ruby
|
|
327
|
+
ActiveSupport::Notifications.subscribe("subscription_renewed.przelewy24") do |*, payload|
|
|
328
|
+
payload[:subscription].owner.extend_access! # payload[:payment] is the paid renewal
|
|
329
|
+
end
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Payment callbacks (`on_payment_verified`) also fire for subscription payments; there `payment.payable` is the `Subscription` and `payment.subscription.owner` is your record. Not supported: the BLIK alias update notification (its payload is not documented by Przelewy24); read aliases with `client.blik.aliases`.
|
|
333
|
+
|
|
334
|
+
### Refunds
|
|
335
|
+
|
|
336
|
+
```ruby
|
|
337
|
+
Przelewy24Payment::Refund.request!(payment: payment, amount: BigDecimal("5.00"))
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Refund status notifications arrive at `/przelewy24/webhooks/refund` and update `Refund#status`.
|
|
341
|
+
|
|
342
|
+
### Low-level client
|
|
343
|
+
|
|
344
|
+
```ruby
|
|
345
|
+
client = Przelewy24Payment.client
|
|
346
|
+
client.test_access
|
|
347
|
+
client.payment_methods.list(lang: "pl")
|
|
348
|
+
client.transactions.find_by_session_id("ABC")
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### Instrumentation
|
|
352
|
+
|
|
353
|
+
`ActiveSupport::Notifications` events: `request.przelewy24`, `payment_verified.przelewy24`, `payment_failed.przelewy24`, `refund_completed.przelewy24`, `refund_failed.przelewy24`, `subscription_activated.przelewy24`, `subscription_renewed.przelewy24`, `subscription_past_due.przelewy24`, `subscription_canceled.przelewy24`. Secrets (`sign`, `crc_key`, `api_key`) are added to `filter_parameters`.
|
|
354
|
+
|
|
355
|
+
`payment_verified` and `payment_failed` are published right after the callback succeeds (payload: `payment:`), `refund_*` events carry `refund:`.
|
|
356
|
+
|
|
357
|
+
```ruby
|
|
358
|
+
ActiveSupport::Notifications.subscribe("payment_verified.przelewy24") do |*, payload|
|
|
359
|
+
Rails.logger.info("Paid: #{payload[:payment].session_id}")
|
|
360
|
+
end
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### Testing in your app
|
|
364
|
+
|
|
365
|
+
```ruby
|
|
366
|
+
payload = Przelewy24Payment::TestHelpers.status_payload(payment)
|
|
367
|
+
post "/przelewy24/webhooks/status", params: payload, as: :json
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
In development expose the app with a tunnel (ngrok, cloudflared) and set `url_options` to its host.
|
|
371
|
+
|
|
372
|
+
## Upgrading from 0.x / 1.x
|
|
373
|
+
|
|
374
|
+
2.0 is a full rewrite and not compatible with earlier versions. Remove the old configuration, run the install generator and migrate order-specific payment columns (`session_id`, `external_id`) to `Przelewy24Payment::Payment`.
|
|
375
|
+
|
|
376
|
+
## Development
|
|
377
|
+
|
|
378
|
+
```bash
|
|
379
|
+
bundle install
|
|
380
|
+
bundle exec rake # specs
|
|
381
|
+
bundle exec rubocop
|
|
382
|
+
```
|
|
173
383
|
|
|
174
|
-
|
|
175
|
-
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
176
|
-
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
177
|
-
4. Push to the branch (`git push origin my-new-feature`)
|
|
178
|
-
5. Create new Pull Request
|
|
384
|
+
MIT licensed.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Przelewy24Payment
|
|
4
|
+
# Inherits from config.parent_controller (default ActionController::API).
|
|
5
|
+
# Do not point it at a controller that requires a signed-in user: Przelewy24
|
|
6
|
+
# calls these endpoints without a session.
|
|
7
|
+
class WebhooksController < Przelewy24Payment.configuration.parent_controller.constantize
|
|
8
|
+
skip_forgery_protection if respond_to?(:skip_forgery_protection)
|
|
9
|
+
|
|
10
|
+
before_action :restrict_ip
|
|
11
|
+
|
|
12
|
+
# POST /przelewy24/webhooks/status
|
|
13
|
+
def status
|
|
14
|
+
notification = Notification.new(webhook_params).verify!
|
|
15
|
+
payment = Payment.find_by!(session_id: notification.session_id)
|
|
16
|
+
|
|
17
|
+
payment.settle!(notification)
|
|
18
|
+
payment.deliver_callbacks!
|
|
19
|
+
head :ok
|
|
20
|
+
rescue InvalidNotificationError
|
|
21
|
+
head :bad_request
|
|
22
|
+
rescue SignatureError
|
|
23
|
+
head :unauthorized
|
|
24
|
+
rescue ActiveRecord::RecordNotFound
|
|
25
|
+
head :not_found
|
|
26
|
+
rescue StandardError => e
|
|
27
|
+
# Includes errors raised by user callbacks: answer 5xx so Przelewy24 retries.
|
|
28
|
+
Przelewy24Payment.configuration.logger&.error("[Przelewy24] webhook failed: #{e.class}: #{e.message}")
|
|
29
|
+
head :internal_server_error
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# POST /przelewy24/webhooks/refund
|
|
33
|
+
def refund
|
|
34
|
+
notification = RefundNotification.new(webhook_params).verify!
|
|
35
|
+
refund = Refund.find_by!(refunds_uuid: notification.refunds_uuid)
|
|
36
|
+
|
|
37
|
+
refund.update!(status: notification.success? ? "completed" : "failed")
|
|
38
|
+
Instrumentation.instrument("refund_#{refund.status}", refund: refund)
|
|
39
|
+
Przelewy24Payment.configuration.on_refund&.call(refund)
|
|
40
|
+
head :ok
|
|
41
|
+
rescue InvalidNotificationError
|
|
42
|
+
head :bad_request
|
|
43
|
+
rescue SignatureError
|
|
44
|
+
head :unauthorized
|
|
45
|
+
rescue ActiveRecord::RecordNotFound
|
|
46
|
+
head :not_found
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
# Raw JSON body is the source of truth; Rails-added keys are dropped.
|
|
52
|
+
def webhook_params
|
|
53
|
+
request.request_parameters.except("controller", "action", "webhook")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def restrict_ip
|
|
57
|
+
allowed = Przelewy24Payment.configuration.allowed_ips
|
|
58
|
+
return if allowed.blank? || allowed.any? { |range| IPAddr.new(range).include?(request.remote_ip) }
|
|
59
|
+
|
|
60
|
+
head :forbidden
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|