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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 50914b4891648701b6e1d7e553820f3938c232af1e52666ddc99c60fc12e22e6
4
- data.tar.gz: b893ef2af65fadf32e9dc5e437a4303f9f538e597751a976378046d87410ae38
3
+ metadata.gz: 0c82aff9dd09e95432ad62c5aedb6b4a2d8d4e9e42d9f17bac37ac7cbebbc90f
4
+ data.tar.gz: e859b6ca00740c0112aa80c117b3584dc2ae64f3be8aceb5fdc8cc055a2bdf59
5
5
  SHA512:
6
- metadata.gz: 6518bb755f204d0e71c717b7c4e4ee1c1cc325596fb43dff3b8d034488d6489f4729d7e798e93ca3d2d186d57f19b02ab1159a38ba68a14a4090affc579cced3
7
- data.tar.gz: 71e3faefb42675a382fe7eb872ab5f08e96843d1fe54a46770c2f75e986fa0950837d1d69e415670014bb0bd46e44245fbfcd27fa6a74d33843a29ea0ab3914b
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. [Error Handling](#error-handling)
11
- 5. [Testing](#testing)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DefraRubyGovpay
4
- VERSION = "0.2.5"
4
+ VERSION = "0.2.6"
5
5
  end
@@ -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.5
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: 2023-11-16 00:00:00.000000000 Z
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