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.
Files changed (48) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +11 -0
  3. data/LICENSE.txt +17 -18
  4. data/README.md +345 -139
  5. data/app/controllers/przelewy24_payment/webhooks_controller.rb +63 -0
  6. data/app/jobs/przelewy24_payment/application_job.rb +6 -0
  7. data/app/jobs/przelewy24_payment/charge_due_subscriptions_job.rb +13 -0
  8. data/app/models/przelewy24_payment/application_record.rb +7 -0
  9. data/app/models/przelewy24_payment/payment.rb +177 -0
  10. data/app/models/przelewy24_payment/refund.rb +33 -0
  11. data/app/models/przelewy24_payment/subscription.rb +147 -0
  12. data/config/routes.rb +6 -0
  13. data/lib/generators/przelewy24_payment/install/install_generator.rb +60 -5
  14. data/lib/generators/przelewy24_payment/install/templates/README +14 -0
  15. data/lib/generators/przelewy24_payment/install/templates/create_przelewy24_payments.rb.tt +21 -0
  16. data/lib/generators/przelewy24_payment/install/templates/create_przelewy24_refunds.rb.tt +14 -0
  17. data/lib/generators/przelewy24_payment/install/templates/create_przelewy24_subscriptions.rb.tt +28 -0
  18. data/lib/generators/przelewy24_payment/install/templates/initializer.rb.tt +47 -0
  19. data/lib/przelewy24_payment/api/base.rb +17 -0
  20. data/lib/przelewy24_payment/api/blik.rb +41 -0
  21. data/lib/przelewy24_payment/api/cards.rb +26 -0
  22. data/lib/przelewy24_payment/api/payment_methods.rb +11 -0
  23. data/lib/przelewy24_payment/api/refunds.rb +25 -0
  24. data/lib/przelewy24_payment/api/transactions.rb +51 -0
  25. data/lib/przelewy24_payment/client.rb +66 -0
  26. data/lib/przelewy24_payment/configuration.rb +81 -0
  27. data/lib/przelewy24_payment/engine.rb +12 -1
  28. data/lib/przelewy24_payment/errors.rb +45 -0
  29. data/lib/przelewy24_payment/instrumentation.rb +16 -0
  30. data/lib/przelewy24_payment/money.rb +26 -0
  31. data/lib/przelewy24_payment/notification.rb +55 -0
  32. data/lib/przelewy24_payment/payable.rb +20 -0
  33. data/lib/przelewy24_payment/refund_notification.rb +44 -0
  34. data/lib/przelewy24_payment/signature.rb +64 -0
  35. data/lib/przelewy24_payment/test_helpers.rb +20 -0
  36. data/lib/przelewy24_payment/version.rb +3 -1
  37. data/lib/przelewy24_payment.rb +26 -187
  38. metadata +83 -24
  39. data/.gitignore +0 -1
  40. data/Gemfile +0 -4
  41. data/Rakefile +0 -2
  42. data/app/helpers/przelewy24_payment/payment_helper.rb +0 -5
  43. data/app/views/przelewy24_payment/_payment_form.html.haml +0 -28
  44. data/lib/generators/przelewy24_payment/install/USAGE +0 -8
  45. data/lib/generators/przelewy24_payment/install/templates/przelewy24_payment.rb +0 -16
  46. data/lib/przelewy24_payment/przelewy24_payment_controller.rb +0 -67
  47. data/lib/tasks/test_connection.rake +0 -18
  48. data/przelewy24_payment.gemspec +0 -19
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: ef6372b0ee892f95bae1a5cbd55216b1a8ef1422
4
- data.tar.gz: 24ac8d0e94cbdb452fc8b451fa450fb637b4248d
2
+ SHA256:
3
+ metadata.gz: e190d58b341f9dcaf800cd181bddb65b06f6dd6841a18d64575441bf8a2d5d9e
4
+ data.tar.gz: b18a7a62dc2304162bee55c61a160bd03e87e55c47b1178c3ed85c692876031f
5
5
  SHA512:
6
- metadata.gz: ca83a8e926443b7df9690f4a3e6190248b63157b051a87e87ee5345be32131541072875e9fc26081e2e3b51e557e28c59bf529699d108e704966126c7ec97a26
7
- data.tar.gz: 663cc01f72c1ebf01ce4a34b4d4664a98850a2d5437afdc4b93b0b576a51705bb65b9a2eedab9d8051e243a597d9df6230130fdd2265d27f6b656e2a3f31bf12
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
- Permission is hereby granted, free of charge, to any person obtaining
6
- a copy of this software and associated documentation files (the
7
- "Software"), to deal in the Software without restriction, including
8
- without limitation the rights to use, copy, modify, merge, publish,
9
- distribute, sublicense, and/or sell copies of the Software, and to
10
- permit persons to whom the Software is furnished to do so, subject to
11
- the following conditions:
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
- included in all copies or substantial portions of the Software.
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
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
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
- # Przelewy24Payment
1
+ # przelewy24_payment
2
2
 
3
- It's rails gem to integrate polish payment method: przelewy24.pl
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 this line to your application's Gemfile:
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
- gem 'przelewy24_payment'
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
- And then execute:
37
+ Then:
12
38
 
13
- $ bundle install
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
- Or install it yourself as:
43
+ The engine works with any model: the payment is linked polymorphically, so no columns are added to your own tables.
16
44
 
17
- $ gem install przelewy24_payment
45
+ ## Configuration
18
46
 
19
- After this create przelewy24_payment config:
47
+ All settings live in `config/initializers/przelewy24_payment.rb`. Values passed there win over ENV.
20
48
 
21
- $ rails g przelewy24_payment:install
49
+ ### Customizing
22
50
 
23
- And you can there "config/initializers/przelewy24_payment.rb" setup your settings:
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
- Przelewy24Payment.setup do |config|
27
- config.merchant_id = 'your_merchant_id'
28
- config.pos_id = 'your_shop_id_default_merchant_id'
29
- config.crc_key = 'crc_key'
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
- Test connection
46
- ```
47
- rake przelewy24_payment:test_connection
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
- Your controller e.g 'PaymentController' should only include:
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 YourPaymentController < ApplicationController
55
- include Przelewy24PaymentController
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
- And you should also have this methods in your payment controller:
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
- class YourPaymentController < ApplicationController
63
- include Przelewy24PaymentController
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
- # after success payemnt this method will be trigger
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
- # e.g
82
- # payment = Payment.find_by_session_id(payment_params[:p24_session_id])
83
- end
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
- # after error payment this method will be trigger
86
- # so you can do whatever you want
87
- def payment_error(payment_params, code, description)
88
- # payment_params returns hash with:
89
- # p24_merchant_id
90
- # p24_pos_id
91
- # p24_session_id
92
- # p24_order_id
93
- # p24_amount
94
- # p24_currency
95
- # p24_method
96
- # p24_sign
97
- # p24_karta
98
- # payment_id
99
- #
100
- # code return error code
101
- # description return error description
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
- # method to setup params to verify it final verifyciation
105
- # so you can do whatever you want
106
- def payment_verify(response_params)
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
- Last step, on your payment view e.g 'app/views/YourController/your_payment.html.haml' you should add:
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
- Finish :)
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
- ## Contributing
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
- 1. Fork it
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
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Przelewy24Payment
4
+ class ApplicationJob < ::ActiveJob::Base
5
+ end
6
+ end