defra_ruby_govpay 0.2.4 → 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: 6040a654f7b5f443c957b0aae87e874d050986040c730544b2ea5edd0378a176
4
- data.tar.gz: 97a4d9020a1390e37a4d20fd797f876f1fc73fd2991c1634b9f2332642da428b
3
+ metadata.gz: 0c82aff9dd09e95432ad62c5aedb6b4a2d8d4e9e42d9f17bac37ac7cbebbc90f
4
+ data.tar.gz: e859b6ca00740c0112aa80c117b3584dc2ae64f3be8aceb5fdc8cc055a2bdf59
5
5
  SHA512:
6
- metadata.gz: f5a78da8b85f0d06bd92933428b676d3b1b3df7a260999ff0c717d1b577d89d5d4d1b3887e58120b21931574c0c17ae386fde16d6200645eccbc72e466e12200
7
- data.tar.gz: f75230f02be4a3e347a78e1b7aaecdf7d06dea04c8d1f8164ed7d02332a4c32729e7b115206295b5ac03a2ed4082bd88c76b6ca20e1e9259ad2a5a5504739e2d
6
+ metadata.gz: 62772d1e4dd940a2faa216a9c53d9e1c3fbbe1a36e04bfc7faa9af5969550c2a9ad1c5b3e8ca75d47052b3689a76618cd653d78d357f3d8c4c2409f17fbc3e3d
7
+ data.tar.gz: 1134a7ff08a8445cca67f50330f022a75e4e51fa08b13c0ef4e1c499659f9c88acf93397c1a1e5b963db4e953c8fd16f65456819a2c50257dc1ce260d5d30ad9
data/CHANGELOG.md CHANGED
@@ -2,7 +2,15 @@
2
2
 
3
3
  ## [Unreleased](https://github.com/DEFRA/defra-ruby-govpay/tree/HEAD)
4
4
 
5
- [Full Changelog](https://github.com/DEFRA/defra-ruby-govpay/compare/v0.2.3...HEAD)
5
+ [Full Changelog](https://github.com/DEFRA/defra-ruby-govpay/compare/v0.2.4...HEAD)
6
+
7
+ **Fixed bugs:**
8
+
9
+ - fix host\_is\_back\_office [\#11](https://github.com/DEFRA/defra-ruby-govpay/pull/11) ([PaulDoyle-DEFRA](https://github.com/PaulDoyle-DEFRA))
10
+
11
+ ## [v0.2.4](https://github.com/DEFRA/defra-ruby-govpay/tree/v0.2.4) (2023-11-10)
12
+
13
+ [Full Changelog](https://github.com/DEFRA/defra-ruby-govpay/compare/v0.2.3...v0.2.4)
6
14
 
7
15
  **Fixed bugs:**
8
16
 
@@ -10,6 +18,7 @@
10
18
 
11
19
  **Merged pull requests:**
12
20
 
21
+ - Version 0.2.4 [\#10](https://github.com/DEFRA/defra-ruby-govpay/pull/10) ([PaulDoyle-DEFRA](https://github.com/PaulDoyle-DEFRA))
13
22
  - Bump rake from 13.0.6 to 13.1.0 [\#6](https://github.com/DEFRA/defra-ruby-govpay/pull/6) ([dependabot[bot]](https://github.com/apps/dependabot))
14
23
 
15
24
  ## [v0.2.3](https://github.com/DEFRA/defra-ruby-govpay/tree/v0.2.3) (2023-11-07)
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- defra_ruby_govpay (0.2.4)
4
+ defra_ruby_govpay (0.2.5)
5
5
  rest-client (~> 2.1)
6
6
 
7
7
  GEM
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
 
@@ -36,7 +37,6 @@ DefraRubyGovpay.configure do |config|
36
37
  config.govpay_url = 'https://your-govpay-url.com'
37
38
  config.govpay_front_office_api_token = 'your-front-office-token'
38
39
  config.govpay_back_office_api_token = 'your-back-office-token'
39
- config.host_is_back_office = false
40
40
  # ... any other configurations
41
41
  end
42
42
  ```
@@ -49,9 +49,10 @@ Here is a detailed guide on how to use the various components of the `defra-ruby
49
49
 
50
50
  You can send requests to the GovPay API using the `send_request` method. Here's an example:
51
51
 
52
- after having followed the configuration step:
52
+ After having followed the configuration step, create an API instance. This has a mandatory parameter to indicate
53
+ whether the host is a back-office application, in which case any payments it creates will be flagged as MOTO.
53
54
  ```ruby
54
- govpay_api = DefraRubyGovpay::API.new
55
+ govpay_api = DefraRubyGovpay::API.new(host_is_back_office: false)
55
56
 
56
57
  begin
57
58
  response = govpay_api.send_request(
@@ -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
  ```
@@ -3,6 +3,7 @@
3
3
  require "rest-client"
4
4
 
5
5
  module DefraRubyGovpay
6
+
6
7
  # Custom error class to handle Govpay API errors
7
8
  class GovpayApiError < StandardError
8
9
  def initialize(msg = "Govpay API error")
@@ -16,6 +17,10 @@ module DefraRubyGovpay
16
17
 
17
18
  class API
18
19
 
20
+ def initialize(host_is_back_office:)
21
+ @host_is_back_office = host_is_back_office
22
+ end
23
+
19
24
  def send_request(method:, path:, params: nil, is_moto: false)
20
25
  @is_moto = is_moto
21
26
  DefraRubyGovpay.logger.debug build_log_message(method, path, params)
@@ -63,10 +68,6 @@ module DefraRubyGovpay
63
68
  "#{govpay_url}#{path}"
64
69
  end
65
70
 
66
- def back_office_app
67
- @back_office_app ||= DefraRubyGovpay.configuration.host_is_back_office
68
- end
69
-
70
71
  def front_office_token
71
72
  @front_office_token ||= DefraRubyGovpay.configuration.govpay_front_office_api_token
72
73
  end
@@ -76,7 +77,7 @@ module DefraRubyGovpay
76
77
  end
77
78
 
78
79
  def bearer_token
79
- if back_office_app
80
+ if @host_is_back_office
80
81
  @is_moto ? back_office_token : front_office_token
81
82
  else
82
83
  front_office_token
@@ -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, :host_is_back_office
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.4"
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.4
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-10 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