defra_ruby_govpay 0.2.5 → 0.2.6
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/README.md +51 -2
- data/lib/defra_ruby_govpay/callback_validator.rb +25 -0
- data/lib/defra_ruby_govpay/configuration.rb +1 -1
- data/lib/defra_ruby_govpay/services/govpay_webhook_base_service.rb +81 -0
- data/lib/defra_ruby_govpay/services/govpay_webhook_payment_service.rb +46 -0
- data/lib/defra_ruby_govpay/services/govpay_webhook_refund_service.rb +50 -0
- data/lib/defra_ruby_govpay/services/govpay_webhook_sanitizer_service.rb +25 -0
- data/lib/defra_ruby_govpay/version.rb +1 -1
- data/lib/defra_ruby_govpay.rb +6 -0
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0c82aff9dd09e95432ad62c5aedb6b4a2d8d4e9e42d9f17bac37ac7cbebbc90f
|
4
|
+
data.tar.gz: e859b6ca00740c0112aa80c117b3584dc2ae64f3be8aceb5fdc8cc055a2bdf59
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 62772d1e4dd940a2faa216a9c53d9e1c3fbbe1a36e04bfc7faa9af5969550c2a9ad1c5b3e8ca75d47052b3689a76618cd653d78d357f3d8c4c2409f17fbc3e3d
|
7
|
+
data.tar.gz: 1134a7ff08a8445cca67f50330f022a75e4e51fa08b13c0ef4e1c499659f9c88acf93397c1a1e5b963db4e953c8fd16f65456819a2c50257dc1ce260d5d30ad9
|
data/README.md
CHANGED
@@ -7,8 +7,9 @@ The `defra-ruby-govpay` gem facilitates seamless integration with GovPay service
|
|
7
7
|
1. [Installation](#installation)
|
8
8
|
2. [Configuration](#configuration)
|
9
9
|
3. [Usage](#usage)
|
10
|
-
4. [
|
11
|
-
5. [
|
10
|
+
4. [Webhook Handling](#webhook-handling)
|
11
|
+
5. [Error Handling](#error-handling)
|
12
|
+
6. [Testing](#testing)
|
12
13
|
|
13
14
|
## Installation
|
14
15
|
|
@@ -77,6 +78,54 @@ rescue DefraRubyGovpay::GovpayApiError => e
|
|
77
78
|
end
|
78
79
|
```
|
79
80
|
|
81
|
+
## Webhook Handling
|
82
|
+
|
83
|
+
The gem provides functionality for handling Govpay webhooks for both payments and refunds. The webhook services validate the webhook content, that the status transition is allowed, return structured information that your application can use to update its records.
|
84
|
+
|
85
|
+
### Processing Webhooks
|
86
|
+
|
87
|
+
The webhook services extract and return data from the webhook payload:
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
# For payment webhooks
|
91
|
+
result = DefraRubyGovpay::GovpayWebhookPaymentService.run(webhook_body)
|
92
|
+
# => { id: "hu20sqlact5260q2nanm0q8u93", status: "success" }
|
93
|
+
|
94
|
+
# For refund webhooks
|
95
|
+
result = DefraRubyGovpay::GovpayWebhookRefundService.run(webhook_body)
|
96
|
+
# => { id: "789", payment_id: "original-payment-123", status: "success" }
|
97
|
+
```
|
98
|
+
|
99
|
+
### Validating Webhook Signatures
|
100
|
+
|
101
|
+
To validate the signature of a webhook, use the `CallbackValidator` class:
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
valid = DefraRubyGovpay::CallbackValidator.call(
|
105
|
+
request_body,
|
106
|
+
ENV['GOVPAY_WEBHOOK_SIGNING_SECRET'],
|
107
|
+
request.headers['Pay-Signature']
|
108
|
+
)
|
109
|
+
|
110
|
+
if valid
|
111
|
+
# Process the webhook
|
112
|
+
else
|
113
|
+
# Handle invalid signature
|
114
|
+
end
|
115
|
+
```
|
116
|
+
|
117
|
+
### Payment vs Refund Webhooks
|
118
|
+
|
119
|
+
The gem can handle both payment and refund webhooks:
|
120
|
+
|
121
|
+
- **Payment Webhooks**: These have a `resource_type` of "payment" and contain payment status information in `resource.state.status`.
|
122
|
+
- **Refund Webhooks**: These have a `refund_id` field and contain refund status information in the `status` field.
|
123
|
+
|
124
|
+
The appropriate service class will be used based on the webhook type:
|
125
|
+
|
126
|
+
- `GovpayWebhookPaymentService` for payment webhooks
|
127
|
+
- `GovpayWebhookRefundService` for refund webhooks
|
128
|
+
|
80
129
|
## Testing
|
81
130
|
|
82
131
|
```
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "openssl"
|
4
|
+
|
5
|
+
module DefraRubyGovpay
|
6
|
+
class CallbackValidator
|
7
|
+
def self.call(request_body, signing_secret, pay_signature_header)
|
8
|
+
new(request_body, signing_secret, pay_signature_header).call
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :request_body, :signing_secret, :pay_signature_header
|
12
|
+
|
13
|
+
def initialize(request_body, signing_secret, pay_signature_header)
|
14
|
+
@request_body = request_body
|
15
|
+
@signing_secret = signing_secret
|
16
|
+
@pay_signature_header = pay_signature_header
|
17
|
+
end
|
18
|
+
|
19
|
+
def call
|
20
|
+
hmac = OpenSSL::HMAC.hexdigest("sha256", signing_secret.encode("utf-8"), request_body.encode("utf-8"))
|
21
|
+
|
22
|
+
hmac == pay_signature_header
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -5,6 +5,6 @@ module DefraRubyGovpay
|
|
5
5
|
# for the DefraRubyGovpay module. You can set different options like
|
6
6
|
# API tokens, host preferences, and other necessary configurations here.
|
7
7
|
class Configuration
|
8
|
-
attr_accessor :govpay_url, :govpay_front_office_api_token, :govpay_back_office_api_token
|
8
|
+
attr_accessor :govpay_url, :govpay_front_office_api_token, :govpay_back_office_api_token, :logger
|
9
9
|
end
|
10
10
|
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/object/blank"
|
4
|
+
|
5
|
+
module DefraRubyGovpay
|
6
|
+
class GovpayWebhookBaseService
|
7
|
+
class InvalidGovpayStatusTransition < StandardError; end
|
8
|
+
|
9
|
+
attr_accessor :webhook_body, :previous_status
|
10
|
+
|
11
|
+
# override this in subclasses
|
12
|
+
VALID_STATUS_TRANSITIONS = {}.freeze
|
13
|
+
|
14
|
+
def self.run(webhook_body, previous_status: nil)
|
15
|
+
new.run(webhook_body, previous_status: previous_status)
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
# No initialization needed
|
20
|
+
end
|
21
|
+
|
22
|
+
def run(webhook_body, previous_status: nil)
|
23
|
+
@webhook_body = webhook_body
|
24
|
+
@previous_status = previous_status
|
25
|
+
|
26
|
+
validate_webhook_body
|
27
|
+
|
28
|
+
# If we have a previous status and it's different from the current one, validate the transition
|
29
|
+
if previous_status && previous_status != webhook_payment_or_refund_status
|
30
|
+
validate_status_transition
|
31
|
+
else
|
32
|
+
DefraRubyGovpay.logger.warn(
|
33
|
+
"Status \"#{@previous_status}\" unchanged in #{payment_or_refund_str} webhook update " \
|
34
|
+
"#{log_webhook_context}"
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Extract and return data from webhook
|
39
|
+
extract_data_from_webhook
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def validate_status_transition
|
45
|
+
return if self.class::VALID_STATUS_TRANSITIONS[previous_status]&.include?(webhook_payment_or_refund_status)
|
46
|
+
|
47
|
+
raise InvalidGovpayStatusTransition, "Invalid #{payment_or_refund_str} status transition " \
|
48
|
+
"from #{previous_status} to #{webhook_payment_or_refund_status}" \
|
49
|
+
"#{log_webhook_context}"
|
50
|
+
end
|
51
|
+
|
52
|
+
def extract_data_from_webhook
|
53
|
+
{
|
54
|
+
id: webhook_payment_or_refund_id,
|
55
|
+
status: webhook_payment_or_refund_status,
|
56
|
+
webhook_body: webhook_body
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
# The following methods must be implemented in subclasses
|
61
|
+
def payment_or_refund_str
|
62
|
+
raise NotImplementedError
|
63
|
+
end
|
64
|
+
|
65
|
+
def validate_webhook_body
|
66
|
+
raise NotImplementedError
|
67
|
+
end
|
68
|
+
|
69
|
+
def webhook_payment_or_refund_id
|
70
|
+
raise NotImplementedError
|
71
|
+
end
|
72
|
+
|
73
|
+
def webhook_payment_or_refund_status
|
74
|
+
raise NotImplementedError
|
75
|
+
end
|
76
|
+
|
77
|
+
def log_webhook_context
|
78
|
+
raise NotImplementedError
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DefraRubyGovpay
|
4
|
+
class GovpayWebhookPaymentService < GovpayWebhookBaseService
|
5
|
+
|
6
|
+
VALID_STATUS_TRANSITIONS = {
|
7
|
+
"created" => %w[started submitted success failed cancelled error],
|
8
|
+
"started" => %w[submitted success failed cancelled error],
|
9
|
+
"submitted" => %w[success failed cancelled error],
|
10
|
+
"success" => %w[],
|
11
|
+
"failed" => %w[],
|
12
|
+
"cancelled" => %w[],
|
13
|
+
"error" => %w[]
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def payment_or_refund_str
|
19
|
+
"payment"
|
20
|
+
end
|
21
|
+
|
22
|
+
def validate_webhook_body
|
23
|
+
raise ArgumentError, "Invalid webhook type #{webhook_resource_type}" unless webhook_resource_type == "payment"
|
24
|
+
|
25
|
+
return unless webhook_payment_or_refund_status.blank?
|
26
|
+
|
27
|
+
raise ArgumentError, "Webhook body missing payment status: #{webhook_body}"
|
28
|
+
end
|
29
|
+
|
30
|
+
def webhook_resource_type
|
31
|
+
@webhook_resource_type ||= webhook_body["resource_type"]&.downcase
|
32
|
+
end
|
33
|
+
|
34
|
+
def webhook_payment_or_refund_id
|
35
|
+
@webhook_payment_or_refund_id ||= webhook_body.dig("resource", "payment_id")
|
36
|
+
end
|
37
|
+
|
38
|
+
def webhook_payment_or_refund_status
|
39
|
+
@webhook_payment_or_refund_status ||= webhook_body.dig("resource", "state", "status")
|
40
|
+
end
|
41
|
+
|
42
|
+
def log_webhook_context
|
43
|
+
"for payment #{webhook_payment_or_refund_id}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DefraRubyGovpay
|
4
|
+
class GovpayWebhookRefundService < GovpayWebhookBaseService
|
5
|
+
|
6
|
+
VALID_STATUS_TRANSITIONS = {
|
7
|
+
"submitted" => %w[success],
|
8
|
+
"success" => %w[],
|
9
|
+
"error" => %w[]
|
10
|
+
}.freeze
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def payment_or_refund_str
|
15
|
+
"refund"
|
16
|
+
end
|
17
|
+
|
18
|
+
def validate_webhook_body
|
19
|
+
return if webhook_payment_or_refund_id.present? && webhook_payment_or_refund_status.present?
|
20
|
+
|
21
|
+
raise ArgumentError, "Invalid refund webhook: #{webhook_body}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def webhook_payment_id
|
25
|
+
@webhook_payment_id ||= webhook_body["payment_id"]
|
26
|
+
end
|
27
|
+
|
28
|
+
def webhook_payment_or_refund_id
|
29
|
+
@webhook_payment_or_refund_id ||= webhook_body["refund_id"]
|
30
|
+
end
|
31
|
+
|
32
|
+
def webhook_payment_or_refund_status
|
33
|
+
@webhook_payment_or_refund_status ||= webhook_body["status"]
|
34
|
+
end
|
35
|
+
|
36
|
+
def extract_data_from_webhook
|
37
|
+
data = super
|
38
|
+
|
39
|
+
data.merge!(
|
40
|
+
payment_id: webhook_payment_id
|
41
|
+
)
|
42
|
+
|
43
|
+
data
|
44
|
+
end
|
45
|
+
|
46
|
+
def log_webhook_context
|
47
|
+
"for refund #{webhook_payment_or_refund_id}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/object/deep_dup"
|
4
|
+
|
5
|
+
module DefraRubyGovpay
|
6
|
+
class GovpayWebhookSanitizerService
|
7
|
+
def self.call(webhook_body)
|
8
|
+
new.call(webhook_body)
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(webhook_body)
|
12
|
+
return webhook_body unless webhook_body.is_a?(Hash)
|
13
|
+
|
14
|
+
# Create a deep copy to avoid modifying the original hash
|
15
|
+
sanitized = webhook_body.deep_dup
|
16
|
+
|
17
|
+
if sanitized["resource"].is_a?(Hash)
|
18
|
+
sanitized["resource"].delete("email")
|
19
|
+
sanitized["resource"].delete("card_details")
|
20
|
+
end
|
21
|
+
|
22
|
+
sanitized
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/defra_ruby_govpay.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "json"
|
4
|
+
require "logger"
|
4
5
|
require_relative "defra_ruby_govpay/version"
|
5
6
|
require_relative "defra_ruby_govpay/configuration"
|
6
7
|
require_relative "defra_ruby_govpay/object"
|
@@ -8,6 +9,11 @@ require_relative "defra_ruby_govpay/payment"
|
|
8
9
|
require_relative "defra_ruby_govpay/refund"
|
9
10
|
require_relative "defra_ruby_govpay/error"
|
10
11
|
require_relative "defra_ruby_govpay/api"
|
12
|
+
require_relative "defra_ruby_govpay/callback_validator"
|
13
|
+
require_relative "defra_ruby_govpay/services/govpay_webhook_base_service"
|
14
|
+
require_relative "defra_ruby_govpay/services/govpay_webhook_payment_service"
|
15
|
+
require_relative "defra_ruby_govpay/services/govpay_webhook_refund_service"
|
16
|
+
require_relative "defra_ruby_govpay/services/govpay_webhook_sanitizer_service"
|
11
17
|
|
12
18
|
# The DefraRubyGovpay module facilitates integration with Govpay services.
|
13
19
|
# It provides a convenient and configurable way to interact with Govpay APIs in Defra's ruby applications.
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: defra_ruby_govpay
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jerome Pratt
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2025-04-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rest-client
|
@@ -48,11 +48,16 @@ files:
|
|
48
48
|
- defra_ruby_govpay.gemspec
|
49
49
|
- lib/defra_ruby_govpay.rb
|
50
50
|
- lib/defra_ruby_govpay/api.rb
|
51
|
+
- lib/defra_ruby_govpay/callback_validator.rb
|
51
52
|
- lib/defra_ruby_govpay/configuration.rb
|
52
53
|
- lib/defra_ruby_govpay/error.rb
|
53
54
|
- lib/defra_ruby_govpay/object.rb
|
54
55
|
- lib/defra_ruby_govpay/payment.rb
|
55
56
|
- lib/defra_ruby_govpay/refund.rb
|
57
|
+
- lib/defra_ruby_govpay/services/govpay_webhook_base_service.rb
|
58
|
+
- lib/defra_ruby_govpay/services/govpay_webhook_payment_service.rb
|
59
|
+
- lib/defra_ruby_govpay/services/govpay_webhook_refund_service.rb
|
60
|
+
- lib/defra_ruby_govpay/services/govpay_webhook_sanitizer_service.rb
|
56
61
|
- lib/defra_ruby_govpay/version.rb
|
57
62
|
- sig/defra_ruby_govpay.rbs
|
58
63
|
homepage: https://github.com/DEFRA/defra-ruby-govpay
|