payu_pl 0.2.0 → 0.3.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/CHANGELOG.md +45 -0
- data/DOCUMENTATION_UPDATES.md +144 -0
- data/README.md +149 -0
- data/WEBHOOK_INTEGRATION_SUMMARY.md +153 -0
- data/examples/WEBHOOK_GUIDE.md +383 -0
- data/examples/rack_webhook_example.ru +66 -0
- data/examples/sinatra_webhook_example.rb +58 -0
- data/examples/webhooks_controller.rb +104 -0
- data/lib/payu_pl/configuration.rb +2 -1
- data/lib/payu_pl/version.rb +1 -1
- data/lib/payu_pl/webhooks/result.rb +32 -0
- data/lib/payu_pl/webhooks/validator.rb +236 -0
- data/lib/payu_pl.rb +3 -0
- metadata +9 -1
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
# PayU Webhook Integration Guide
|
|
2
|
+
|
|
3
|
+
This guide shows how to integrate PayU webhook validation into your application.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
PayU sends webhook notifications when order status changes. Each webhook includes:
|
|
8
|
+
- Order details (ID, status, amount, etc.)
|
|
9
|
+
- A signature header for verification
|
|
10
|
+
- JSON payload with the order data
|
|
11
|
+
|
|
12
|
+
The `PayuPl::Webhooks::Validator` class handles signature verification and payload parsing.
|
|
13
|
+
|
|
14
|
+
## Configuration
|
|
15
|
+
|
|
16
|
+
### Option 1: Via Initializer (Recommended for Rails)
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
# config/initializers/payu.rb
|
|
20
|
+
PayuPl.configure do |config|
|
|
21
|
+
config.second_key = ENV.fetch('PAYU_SECOND_KEY')
|
|
22
|
+
end
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Option 2: Via Environment Variable
|
|
26
|
+
|
|
27
|
+
Set `PAYU_SECOND_KEY` in your environment:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
export PAYU_SECOND_KEY=your_second_key_here
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The validator will automatically use this if no other configuration is provided.
|
|
34
|
+
|
|
35
|
+
### Option 3: Pass Directly
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
validator = PayuPl::Webhooks::Validator.new(request, second_key: 'custom_key')
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Finding Your Second Key
|
|
42
|
+
|
|
43
|
+
1. Log in to PayU merchant panel
|
|
44
|
+
2. Go to Settings > POS Configuration
|
|
45
|
+
3. Find "Second Key" (also called "MD5 Key")
|
|
46
|
+
4. This is the secret used for webhook signature verification
|
|
47
|
+
|
|
48
|
+
**Important**: Keep this key secure! Never commit it to version control.
|
|
49
|
+
|
|
50
|
+
## Basic Usage
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
# Create validator with request object
|
|
54
|
+
validator = PayuPl::Webhooks::Validator.new(request)
|
|
55
|
+
|
|
56
|
+
# Validate and parse in one step
|
|
57
|
+
result = validator.validate_and_parse
|
|
58
|
+
|
|
59
|
+
if result.success?
|
|
60
|
+
payload = result.data
|
|
61
|
+
order_id = payload.dig('order', 'orderId')
|
|
62
|
+
status = payload.dig('order', 'status')
|
|
63
|
+
|
|
64
|
+
# Convert amount from minor units to major units
|
|
65
|
+
amount_minor = payload.dig('order', 'totalAmount').to_i # e.g., 2900
|
|
66
|
+
amount_major = amount_minor / 100.0 # e.g., 29.00
|
|
67
|
+
currency = payload.dig('order', 'currencyCode') # e.g., "PLN"
|
|
68
|
+
|
|
69
|
+
logger.info("Order #{order_id}: #{amount_major} #{currency}")
|
|
70
|
+
# Process webhook...
|
|
71
|
+
else
|
|
72
|
+
# Handle validation error
|
|
73
|
+
logger.error("Webhook validation failed: #{result.error}")
|
|
74
|
+
end
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Framework-Specific Examples
|
|
78
|
+
|
|
79
|
+
### Rails
|
|
80
|
+
|
|
81
|
+
See [webhooks_controller.rb](webhooks_controller.rb) for a complete Rails example.
|
|
82
|
+
|
|
83
|
+
Key points:
|
|
84
|
+
- Skip CSRF token verification
|
|
85
|
+
- Use logger for debugging
|
|
86
|
+
- Handle duplicates
|
|
87
|
+
- Process asynchronously
|
|
88
|
+
|
|
89
|
+
### Sinatra
|
|
90
|
+
|
|
91
|
+
See [sinatra_webhook_example.rb](sinatra_webhook_example.rb) for a complete Sinatra example.
|
|
92
|
+
|
|
93
|
+
Key points:
|
|
94
|
+
- Simple route definition
|
|
95
|
+
- Direct request handling
|
|
96
|
+
- Minimal boilerplate
|
|
97
|
+
|
|
98
|
+
### Rack
|
|
99
|
+
|
|
100
|
+
See [rack_webhook_example.ru](rack_webhook_example.ru) for a complete Rack example.
|
|
101
|
+
|
|
102
|
+
Key points:
|
|
103
|
+
- Works with any Rack-compatible framework
|
|
104
|
+
- No framework dependencies
|
|
105
|
+
- Easy to integrate
|
|
106
|
+
|
|
107
|
+
## Webhook Payload Structure
|
|
108
|
+
|
|
109
|
+
PayU sends notifications in JSON format using POST method.
|
|
110
|
+
|
|
111
|
+
Example payload from PayU:
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"order": {
|
|
116
|
+
"orderId": "WZHF5FFDRJ140731GUEST000P01",
|
|
117
|
+
"extOrderId": "your-order-id-123",
|
|
118
|
+
"orderCreateDate": "2024-01-15T10:30:00.000Z",
|
|
119
|
+
"notifyUrl": "https://your-app.com/webhooks/payu",
|
|
120
|
+
"customerIp": "127.0.0.1",
|
|
121
|
+
"merchantPosId": "300000",
|
|
122
|
+
"description": "Order description",
|
|
123
|
+
"currencyCode": "PLN",
|
|
124
|
+
"totalAmount": "21000",
|
|
125
|
+
"status": "COMPLETED",
|
|
126
|
+
"products": [
|
|
127
|
+
{
|
|
128
|
+
"name": "Product Name",
|
|
129
|
+
"unitPrice": "21000",
|
|
130
|
+
"quantity": "1"
|
|
131
|
+
}
|
|
132
|
+
]
|
|
133
|
+
},
|
|
134
|
+
"localReceiptDateTime": "2024-01-15T10:35:00.000Z",
|
|
135
|
+
"properties": [
|
|
136
|
+
{
|
|
137
|
+
"name": "PAYMENT_ID",
|
|
138
|
+
"value": "123456789"
|
|
139
|
+
}
|
|
140
|
+
]
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Important Payload Notes**:
|
|
145
|
+
|
|
146
|
+
- `totalAmount` / `unitPrice` - **Always in minor units** (e.g., 2900 = 29.00 PLN, 1050 = 10.50 EUR)
|
|
147
|
+
- Divide by 100 to convert to major currency units
|
|
148
|
+
- Example: `"totalAmount": "2900"` means 29.00 PLN
|
|
149
|
+
- `localReceiptDateTime` - Only present for **COMPLETED** status
|
|
150
|
+
- `properties[PAYMENT_ID]` - Payment identifier shown on transaction statements (Trans ID in management panel)
|
|
151
|
+
- `payMethod.type` - Payment method used (if present):
|
|
152
|
+
- `PBL` - Online/standard transfer
|
|
153
|
+
- `CARD_TOKEN` - Card payment
|
|
154
|
+
- `INSTALLMENTS` - PayU Installments
|
|
155
|
+
|
|
156
|
+
## Order Status Values
|
|
157
|
+
|
|
158
|
+
PayU sends notifications for orders in these statuses:
|
|
159
|
+
|
|
160
|
+
- `PENDING` - Payment is currently being processed
|
|
161
|
+
- `WAITING_FOR_CONFIRMATION` - PayU is waiting for merchant to capture payment (when auto-receive is disabled)
|
|
162
|
+
- `COMPLETED` - Payment accepted, funds will be paid out shortly
|
|
163
|
+
- `CANCELED` - Payment cancelled, buyer was not charged
|
|
164
|
+
|
|
165
|
+
## PayU Notification Retry Mechanism
|
|
166
|
+
|
|
167
|
+
PayU expects a **200 HTTP status code** response. If a different status is received, PayU will retry sending the notification up to **20 times** with increasing intervals:
|
|
168
|
+
|
|
169
|
+
| Attempt | Retry After |
|
|
170
|
+
|---------|-------------|
|
|
171
|
+
| 1 | Immediately |
|
|
172
|
+
| 2 | 1 minute |
|
|
173
|
+
| 3 | 2 minutes |
|
|
174
|
+
| 4 | 5 minutes |
|
|
175
|
+
| 5 | 10 minutes |
|
|
176
|
+
| 6 | 30 minutes |
|
|
177
|
+
| 7 | 1 hour |
|
|
178
|
+
| 8-20 | 2-72 hours |
|
|
179
|
+
|
|
180
|
+
**Important**: Always return 200 OK to acknowledge receipt, even if processing fails internally. Process errors should be handled asynchronously.
|
|
181
|
+
|
|
182
|
+
## IP Address Whitelisting
|
|
183
|
+
|
|
184
|
+
If you filter incoming requests by IP, allow these PayU addresses:
|
|
185
|
+
|
|
186
|
+
### Production
|
|
187
|
+
```
|
|
188
|
+
185.68.12.10, 185.68.12.11, 185.68.12.12
|
|
189
|
+
185.68.12.26, 185.68.12.27, 185.68.12.28
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Sandbox
|
|
193
|
+
```
|
|
194
|
+
185.68.14.10, 185.68.14.11, 185.68.14.12
|
|
195
|
+
185.68.14.26, 185.68.14.27, 185.68.14.28
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Notification Headers
|
|
199
|
+
|
|
200
|
+
PayU sends these headers with each notification:
|
|
201
|
+
|
|
202
|
+
- `OpenPayu-Signature` - Signature for verification (also sent as `X-OpenPayU-Signature`)
|
|
203
|
+
- `Content-Type` - Always `application/json;charset=UTF-8`
|
|
204
|
+
- `Authorization` - Basic auth credentials
|
|
205
|
+
- `PayU-Processing-Time` - Time (ms) spent processing at PayU side (selected statuses only)
|
|
206
|
+
|
|
207
|
+
Example signature header:
|
|
208
|
+
```
|
|
209
|
+
OpenPayu-Signature: sender=checkout;signature=d47d8a771d558c29285887febddd9327;algorithm=MD5;content=DOCUMENT
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Best Practices
|
|
213
|
+
|
|
214
|
+
### 1. Handle Duplicates
|
|
215
|
+
|
|
216
|
+
PayU may send the same webhook multiple times due to:
|
|
217
|
+
- Retry mechanism (if you don't return 200)
|
|
218
|
+
- Network issues
|
|
219
|
+
- PayU's intentional duplicate prevention
|
|
220
|
+
|
|
221
|
+
Always check for duplicates and return 200 even for duplicates:
|
|
222
|
+
|
|
223
|
+
```ruby
|
|
224
|
+
event_id = payload['eventId'] || "#{order_id}_#{status}"
|
|
225
|
+
|
|
226
|
+
if WebhookEvent.exists?(event_id: event_id)
|
|
227
|
+
logger.warn("Duplicate webhook: #{event_id}")
|
|
228
|
+
return head :ok # Return 200 to acknowledge
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Store event...
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### 2. Process Asynchronously
|
|
235
|
+
|
|
236
|
+
Don't block the webhook endpoint. Respond quickly and process in background:
|
|
237
|
+
|
|
238
|
+
```ruby
|
|
239
|
+
# Store webhook
|
|
240
|
+
WebhookEvent.create!(event_id: event_id, payload: payload)
|
|
241
|
+
|
|
242
|
+
# Process asynchronously
|
|
243
|
+
PayuWebhookJob.perform_async(event_id)
|
|
244
|
+
|
|
245
|
+
# Respond immediately
|
|
246
|
+
head :ok
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### 3. Return Correct Status Codes
|
|
250
|
+
|
|
251
|
+
**Critical**: Always return `200 OK` after validating the signature, even if your business logic fails.
|
|
252
|
+
|
|
253
|
+
```ruby
|
|
254
|
+
# ✅ GOOD - Return 200 after validation
|
|
255
|
+
result = validator.validate_and_parse
|
|
256
|
+
if result.failure?
|
|
257
|
+
return head :bad_request # Invalid signature
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Store and process asynchronously
|
|
261
|
+
WebhookEvent.create!(payload: result.data)
|
|
262
|
+
head :ok # Return 200 immediately
|
|
263
|
+
|
|
264
|
+
# ❌ BAD - Don't return 500 for business logic errors
|
|
265
|
+
# This will cause PayU to retry unnecessarily
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Status code meanings:
|
|
269
|
+
- `200 OK` - Webhook received and validated (return this even if your processing fails)
|
|
270
|
+
- `400 Bad Request` - Invalid signature/payload only
|
|
271
|
+
- `500 Internal Server Error` - Should be avoided; causes PayU to retry
|
|
272
|
+
|
|
273
|
+
**Note**: If you return non-200, PayU will retry up to 20 times over 72 hours.
|
|
274
|
+
|
|
275
|
+
### 4. Enable Logging
|
|
276
|
+
|
|
277
|
+
Use logging to debug issues:
|
|
278
|
+
|
|
279
|
+
```ruby
|
|
280
|
+
validator = PayuPl::Webhooks::Validator.new(request, logger: Rails.logger)
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
This logs:
|
|
284
|
+
- Signature verification steps
|
|
285
|
+
- Algorithm used
|
|
286
|
+
- Payload parsing
|
|
287
|
+
- Any errors
|
|
288
|
+
|
|
289
|
+
### 5. Test Webhook Integration
|
|
290
|
+
|
|
291
|
+
#### Manual Testing
|
|
292
|
+
|
|
293
|
+
Use curl to simulate a webhook:
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
# Generate signature (example for MD5)
|
|
297
|
+
BODY='{"order":{"orderId":"TEST123","status":"COMPLETED"}}'
|
|
298
|
+
KEY='your_second_key'
|
|
299
|
+
SIGNATURE=$(echo -n "${BODY}${KEY}" | md5sum | cut -d' ' -f1)
|
|
300
|
+
|
|
301
|
+
# Send webhook
|
|
302
|
+
curl -X POST http://localhost:3000/webhooks/payu \
|
|
303
|
+
-H "Content-Type: application/json" \
|
|
304
|
+
-H "OpenPayU-Signature: signature=${SIGNATURE};algorithm=MD5" \
|
|
305
|
+
-d "${BODY}"
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
#### Using PayU Sandbox
|
|
309
|
+
|
|
310
|
+
1. Create an order in sandbox
|
|
311
|
+
2. Configure notifyUrl to your webhook endpoint
|
|
312
|
+
3. PayU will send real webhook notifications
|
|
313
|
+
4. Use a tool like ngrok to expose local server
|
|
314
|
+
|
|
315
|
+
## Signature Algorithms
|
|
316
|
+
|
|
317
|
+
PayU supports multiple signature algorithms:
|
|
318
|
+
|
|
319
|
+
- **MD5** - PayU may use `MD5(body + key)` or `MD5(key + body)`
|
|
320
|
+
- Validator checks both automatically
|
|
321
|
+
- **SHA1** - HMAC-SHA1
|
|
322
|
+
- **SHA256** - HMAC-SHA256 (recommended)
|
|
323
|
+
- **SHA384** - HMAC-SHA384
|
|
324
|
+
- **SHA512** - HMAC-SHA512
|
|
325
|
+
|
|
326
|
+
The validator automatically detects the algorithm from the webhook header.
|
|
327
|
+
|
|
328
|
+
## Troubleshooting
|
|
329
|
+
|
|
330
|
+
### "Missing OpenPayU signature header"
|
|
331
|
+
|
|
332
|
+
- PayU didn't send the signature header
|
|
333
|
+
- Check if request is actually from PayU
|
|
334
|
+
- Verify your notifyUrl is correct
|
|
335
|
+
|
|
336
|
+
### "Signature verification failed"
|
|
337
|
+
|
|
338
|
+
- Wrong second_key configured
|
|
339
|
+
- Payload was modified in transit
|
|
340
|
+
- Algorithm mismatch
|
|
341
|
+
|
|
342
|
+
**Debug steps**:
|
|
343
|
+
1. Enable logging to see expected vs received signature
|
|
344
|
+
2. Verify second_key matches PayU merchant panel
|
|
345
|
+
3. Check raw request body hasn't been modified
|
|
346
|
+
|
|
347
|
+
### "PayU second_key not configured"
|
|
348
|
+
|
|
349
|
+
- No configuration found
|
|
350
|
+
- Set via `PayuPl.configure`, ENV, or pass directly
|
|
351
|
+
|
|
352
|
+
### Tests Failing
|
|
353
|
+
|
|
354
|
+
Ensure rack gem is available for tests:
|
|
355
|
+
|
|
356
|
+
```ruby
|
|
357
|
+
# Gemfile
|
|
358
|
+
group :test do
|
|
359
|
+
gem 'rack', '~> 3.0'
|
|
360
|
+
end
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
## Security Considerations
|
|
364
|
+
|
|
365
|
+
1. **Always verify signatures** - Never trust webhook data without verification
|
|
366
|
+
2. **Keep second_key secret** - Don't commit to version control
|
|
367
|
+
3. **Use HTTPS** - Configure notifyUrl with https://
|
|
368
|
+
4. **Validate IP addresses** (optional) - PayU webhooks come from known IPs
|
|
369
|
+
5. **Rate limiting** (optional) - Protect against abuse
|
|
370
|
+
|
|
371
|
+
## Additional Resources
|
|
372
|
+
|
|
373
|
+
- [PayU Webhook Documentation](https://developers.payu.com/europe/docs/webhooks/)
|
|
374
|
+
- [PayU API Reference](https://developers.payu.com/europe/api/)
|
|
375
|
+
- [Sandbox Testing Guide](https://developers.payu.com/europe/docs/testing/sandbox/)
|
|
376
|
+
|
|
377
|
+
## Support
|
|
378
|
+
|
|
379
|
+
For issues with this gem:
|
|
380
|
+
- GitHub Issues: https://github.com/dawidof/payu_pl/issues
|
|
381
|
+
|
|
382
|
+
For PayU API issues:
|
|
383
|
+
- PayU Support: https://www.payu.pl/pomoc
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Example Rack app for handling PayU webhooks
|
|
4
|
+
#
|
|
5
|
+
# This shows how to use the webhook validator in a plain Rack application
|
|
6
|
+
#
|
|
7
|
+
# To use this:
|
|
8
|
+
# 1. Set PAYU_SECOND_KEY environment variable
|
|
9
|
+
# 2. Run with: rackup examples/rack_webhook_example.ru
|
|
10
|
+
#
|
|
11
|
+
# Or integrate the middleware into your existing Rack app
|
|
12
|
+
|
|
13
|
+
require 'payu_pl'
|
|
14
|
+
require 'json'
|
|
15
|
+
|
|
16
|
+
class PayuWebhookApp
|
|
17
|
+
def call(env)
|
|
18
|
+
request = Rack::Request.new(env)
|
|
19
|
+
|
|
20
|
+
# Only handle POST to /webhooks/payu
|
|
21
|
+
unless request.post? && request.path == '/webhooks/payu'
|
|
22
|
+
return [404, { 'Content-Type' => 'text/plain' }, ['Not Found']]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Validate webhook signature and parse payload
|
|
26
|
+
validator = PayuPl::Webhooks::Validator.new(request)
|
|
27
|
+
result = validator.validate_and_parse
|
|
28
|
+
|
|
29
|
+
if result.failure?
|
|
30
|
+
puts "ERROR: PayU webhook validation failed: #{result.error}"
|
|
31
|
+
return [400, { 'Content-Type' => 'text/plain' }, ['Bad Request']]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Extract webhook data
|
|
35
|
+
payload = result.data
|
|
36
|
+
order_id = payload.dig('order', 'orderId')
|
|
37
|
+
status = payload.dig('order', 'status')
|
|
38
|
+
|
|
39
|
+
puts "INFO: PayU webhook received - Order: #{order_id}, Status: #{status}"
|
|
40
|
+
|
|
41
|
+
# Process the webhook
|
|
42
|
+
# Your business logic here...
|
|
43
|
+
|
|
44
|
+
[200, { 'Content-Type' => 'text/plain' }, ['OK']]
|
|
45
|
+
rescue StandardError => e
|
|
46
|
+
puts "ERROR: PayU webhook processing error: #{e.class} - #{e.message}"
|
|
47
|
+
[500, { 'Content-Type' => 'text/plain' }, ['Internal Server Error']]
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Optional: Add logging middleware
|
|
52
|
+
class LoggingMiddleware
|
|
53
|
+
def initialize(app)
|
|
54
|
+
@app = app
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def call(env)
|
|
58
|
+
request = Rack::Request.new(env)
|
|
59
|
+
puts "#{Time.now} - #{request.request_method} #{request.path}"
|
|
60
|
+
@app.call(env)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Build the Rack app
|
|
65
|
+
use LoggingMiddleware
|
|
66
|
+
run PayuWebhookApp.new
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Example Sinatra app for handling PayU webhooks
|
|
4
|
+
#
|
|
5
|
+
# To use this:
|
|
6
|
+
# 1. Install sinatra: gem install sinatra
|
|
7
|
+
# 2. Set PAYU_SECOND_KEY environment variable
|
|
8
|
+
# 3. Run: ruby examples/sinatra_webhook_example.rb
|
|
9
|
+
#
|
|
10
|
+
# Or integrate into your existing Sinatra app
|
|
11
|
+
|
|
12
|
+
require 'sinatra'
|
|
13
|
+
require 'payu_pl'
|
|
14
|
+
require 'json'
|
|
15
|
+
|
|
16
|
+
# Configure PayU (optional - can also use ENV['PAYU_SECOND_KEY'])
|
|
17
|
+
PayuPl.configure do |config|
|
|
18
|
+
config.second_key = ENV['PAYU_SECOND_KEY']
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# PayU webhook endpoint
|
|
22
|
+
post '/webhooks/payu' do
|
|
23
|
+
# Validate webhook signature and parse payload
|
|
24
|
+
result = PayuPl::Webhooks::Validator.new(request).validate_and_parse
|
|
25
|
+
|
|
26
|
+
if result.failure?
|
|
27
|
+
logger.error("PayU webhook validation failed: #{result.error}")
|
|
28
|
+
status 400
|
|
29
|
+
return
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Extract webhook data
|
|
33
|
+
payload = result.data
|
|
34
|
+
order_id = payload.dig('order', 'orderId')
|
|
35
|
+
status_value = payload.dig('order', 'status')
|
|
36
|
+
|
|
37
|
+
logger.info("PayU webhook - Order: #{order_id}, Status: #{status_value}")
|
|
38
|
+
|
|
39
|
+
# Process the webhook
|
|
40
|
+
# Your business logic here...
|
|
41
|
+
|
|
42
|
+
status 200
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Health check endpoint
|
|
46
|
+
get '/health' do
|
|
47
|
+
content_type :json
|
|
48
|
+
{ status: 'ok' }.to_json
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Start the server (for standalone usage)
|
|
52
|
+
if __FILE__ == $0
|
|
53
|
+
set :port, 4567
|
|
54
|
+
set :bind, '0.0.0.0'
|
|
55
|
+
|
|
56
|
+
puts "PayU webhook listener starting on http://0.0.0.0:4567"
|
|
57
|
+
puts "Webhook endpoint: POST /webhooks/payu"
|
|
58
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Example Rails controller for handling PayU webhooks
|
|
4
|
+
#
|
|
5
|
+
# To use this in your Rails app:
|
|
6
|
+
# 1. Copy this file to app/controllers/webhooks/payu_controller.rb
|
|
7
|
+
# 2. Add route: post '/webhooks/payu', to: 'webhooks/payu#create'
|
|
8
|
+
# 3. Configure PayU second_key in config/initializers/payu.rb
|
|
9
|
+
#
|
|
10
|
+
# Note: This is just an example. Adjust to your application's needs.
|
|
11
|
+
|
|
12
|
+
module Webhooks
|
|
13
|
+
class PayuController < ApplicationController
|
|
14
|
+
# Skip CSRF token verification for webhooks
|
|
15
|
+
skip_before_action :verify_authenticity_token
|
|
16
|
+
|
|
17
|
+
def create
|
|
18
|
+
# Validate webhook signature and parse payload
|
|
19
|
+
result = PayuPl::Webhooks::Validator.new(request, logger: Rails.logger).validate_and_parse
|
|
20
|
+
|
|
21
|
+
if result.failure?
|
|
22
|
+
Rails.logger.error("PayU webhook validation failed: #{result.error}")
|
|
23
|
+
return head :bad_request
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Extract webhook data
|
|
27
|
+
payload = result.data
|
|
28
|
+
order_id = payload.dig('order', 'orderId')
|
|
29
|
+
status = payload.dig('order', 'status')
|
|
30
|
+
event_id = payload['eventId'] || "#{order_id}_#{status}"
|
|
31
|
+
|
|
32
|
+
# Format amount (PayU sends in minor units: 2900 = 29.00 PLN)
|
|
33
|
+
total_amount = payload.dig('order', 'totalAmount').to_i
|
|
34
|
+
currency = payload.dig('order', 'currencyCode')
|
|
35
|
+
formatted_amount = "%.2f" % (total_amount / 100.0)
|
|
36
|
+
|
|
37
|
+
Rails.logger.info("PayU webhook received - Order: #{order_id}, Status: #{status}")
|
|
38
|
+
Rails.logger.info("Amount: #{formatted_amount} #{currency}")
|
|
39
|
+
|
|
40
|
+
# Optional: Log PayU processing time for monitoring
|
|
41
|
+
if request.headers['PayU-Processing-Time']
|
|
42
|
+
Rails.logger.info("PayU processing time: #{request.headers['PayU-Processing-Time']}ms")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Check for duplicate events (optional but recommended)
|
|
46
|
+
# You'll need a WebhookEvent model or similar to track processed events
|
|
47
|
+
# if WebhookEvent.exists?(provider: 'payu', event_id: event_id)
|
|
48
|
+
# Rails.logger.warn("Duplicate PayU webhook event: #{event_id}")
|
|
49
|
+
# return head :ok
|
|
50
|
+
# end
|
|
51
|
+
|
|
52
|
+
# Store the webhook event (optional)
|
|
53
|
+
# WebhookEvent.create!(
|
|
54
|
+
# provider: 'payu',
|
|
55
|
+
# event_id: event_id,
|
|
56
|
+
# event_type: status,
|
|
57
|
+
# payload: payload
|
|
58
|
+
# )
|
|
59
|
+
|
|
60
|
+
# Process the webhook asynchronously (recommended)
|
|
61
|
+
# PayuWebhookJob.perform_async(order_id, status, payload)
|
|
62
|
+
|
|
63
|
+
# Or process synchronously (not recommended for production)
|
|
64
|
+
# process_webhook(order_id, status, payload)
|
|
65
|
+
|
|
66
|
+
head :ok
|
|
67
|
+
rescue StandardError => e
|
|
68
|
+
# Log unexpected errors
|
|
69
|
+
Rails.logger.error("PayU webhook processing error: #{e.class} - #{e.message}")
|
|
70
|
+
Rails.logger.error(e.backtrace.first(5).join("\n"))
|
|
71
|
+
|
|
72
|
+
# Return 500 so PayU will retry
|
|
73
|
+
head :internal_server_error
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def process_webhook(order_id, status, payload)
|
|
79
|
+
# Your business logic here
|
|
80
|
+
# For example:
|
|
81
|
+
# - Update order status in your database
|
|
82
|
+
# - Send confirmation emails
|
|
83
|
+
# - Trigger fulfillment processes
|
|
84
|
+
# - etc.
|
|
85
|
+
|
|
86
|
+
case status
|
|
87
|
+
when 'PENDING'
|
|
88
|
+
Rails.logger.info("Order #{order_id} is pending payment")
|
|
89
|
+
when 'WAITING_FOR_CONFIRMATION'
|
|
90
|
+
Rails.logger.info("Order #{order_id} is waiting for manual capture")
|
|
91
|
+
# This status appears when auto-receive is disabled
|
|
92
|
+
# You need to manually capture or cancel the order
|
|
93
|
+
when 'COMPLETED'
|
|
94
|
+
Rails.logger.info("Order #{order_id} completed successfully")
|
|
95
|
+
# Update your order status, trigger fulfillment, etc.
|
|
96
|
+
when 'CANCELED'
|
|
97
|
+
Rails.logger.info("Order #{order_id} was canceled")
|
|
98
|
+
# Handle cancellation
|
|
99
|
+
else
|
|
100
|
+
Rails.logger.warn("Unknown status #{status} for order #{order_id}")
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
data/lib/payu_pl/version.rb
CHANGED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PayuPl
|
|
4
|
+
module Webhooks
|
|
5
|
+
# Result class for webhook validation responses
|
|
6
|
+
# Provides a consistent interface for success and failure cases
|
|
7
|
+
class Result
|
|
8
|
+
attr_reader :data, :error
|
|
9
|
+
|
|
10
|
+
def initialize(data = nil, error = nil)
|
|
11
|
+
@data = data
|
|
12
|
+
@error = error
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.success(data)
|
|
16
|
+
new(data, nil)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.failure(error)
|
|
20
|
+
new(nil, error)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def success?
|
|
24
|
+
error.nil?
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def failure?
|
|
28
|
+
!success?
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|