govuk-pay-api-client 0.1.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.
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.3.1
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.2.3
5
+ before_install: gem install bundler -v 1.12.5
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
data/README.md ADDED
@@ -0,0 +1,138 @@
1
+ [![CircleCI](https://circleci.com/gh/ministryofjustice/govuk-pay-api-client.svg?style=svg&circle-token=7fcc2b811dde84109b6615aa4e7b6ed89350ea53)](https://circleci.com/gh/ministryofjustice/govuk-pay-api-client)
2
+
3
+ # GovukPayApiClient
4
+
5
+ A simple client to integrate with the Govuk Pay payment gateway.
6
+
7
+ ## Usage
8
+
9
+ ### Create Payment
10
+
11
+ ```ruby
12
+ GovukPayApiClient::CreatePayment.call(<fee object>, <url to return to>)
13
+ ```
14
+
15
+ The first argument is a fee object that must repond to `#description`
16
+ (text), `#reference` (string), and `#amount` (integer, in pence). The
17
+ second object is a url string that tell the Govuk Pay gateway where to
18
+ return the user after a successful payment. Both are required.
19
+
20
+ `GovukPayApiClient::RequiresFeeObject` will be raised if a fee object is
21
+ not supplied.
22
+
23
+ `GovukPayApiClient::RequiresReturnUrl` will be raised if a return url is
24
+ not supplied.
25
+
26
+ ### Get Status
27
+
28
+ ```ruby
29
+ GovukPayApiClient::GetStatus.call(<fee object>)
30
+ ```
31
+
32
+ Requires a fee object that must respond to `#govpay_payment_id`, which
33
+ should return an id that is valid on the Govuk Pay gateway.
34
+
35
+ It returns an object with the method `#status` for a successful calls.
36
+ Unsuccessful calls raise errors in the 400 to 599 status range and will
37
+ therefore raise `GovukPayApiClient::Unavailable` errors.
38
+
39
+ `GovukPayApiClient::RequiresFeeObject` will be raised if a fee object is
40
+ not supplied.
41
+
42
+ ### Errors
43
+
44
+ If any `GovukPayApiClient` post or get request returns a status in the
45
+ 400 to 599 range, the client will raise
46
+ `GovukPayApiClient::Unavailable`. It will also raise
47
+ `GovukPayApiClient::Unavailable` if the connection times out or is
48
+ otherwise unavailable.
49
+
50
+
51
+ ## Examples
52
+
53
+ See the dummy Rails app in `/spec/dummy` for examples of how the gem might
54
+ be used in a production environment.
55
+ ## Installation
56
+
57
+ Add this line to your application's Gemfile:
58
+
59
+ ```ruby
60
+ gem 'govuk-pay-api-client'
61
+ ```
62
+
63
+ And then execute:
64
+
65
+ $ bundle
66
+
67
+ Or install it yourself as:
68
+
69
+ $ gem install govuk-pay-api-client
70
+
71
+ ## Testing
72
+
73
+ Run `bundle rake` in the gem source directory for a full set of specs,
74
+ mutation tests and rubocop checks.
75
+
76
+ ### Shared examples
77
+
78
+ The gem can install a set of shared examples in your app that will stub
79
+ a sensible set of API calls using Excon’s stubbing functionality. To
80
+ install these, install the gem, make sure you have the `spec/support`
81
+ subdirectory then run:
82
+
83
+ ```ruby
84
+ bundle exec rake govuk_pay_api_client:install_shared_examples
85
+ ```
86
+
87
+ This will install `spec/support/shared_examples_for_govpay.rb`.
88
+
89
+ Lastly, add these lines to `spec/rails_helper`:
90
+
91
+ ```ruby
92
+ config.before(:all) do
93
+ Excon.defaults[:mock] = true
94
+ end
95
+
96
+ config.after(:each) do
97
+ Excon.stubs.clear
98
+ end
99
+ ```
100
+
101
+ ## Contributing
102
+
103
+ Fork, then clone the repo:
104
+
105
+ ```bash
106
+ git clone git@github.com:your-username/govuk-pay-api-client.git
107
+ ```
108
+
109
+ Make sure the tests pass:
110
+
111
+ ```bash
112
+ bundle
113
+ bundle db:setup
114
+ bundle exec rake
115
+ ```
116
+
117
+ Make your change. Add specs for your change. Make the specs pass:
118
+
119
+ ```bash
120
+ bundle exec rake
121
+ ```
122
+
123
+ Push to your fork and [submit a pull request][pr].
124
+
125
+ [pr]: https://github.com/ministryofjustice/govuk-pay-api-client/compare
126
+
127
+ Some things that will increase the chance that your pull request is
128
+ accepted:
129
+
130
+ * Write specs.
131
+ * Make sure you don’t have any mutants (part of total test suite).
132
+ * Write a [good commit message][commit].
133
+
134
+ [commit]: https://github.com/alphagov/styleguides/blob/master/git.md
135
+
136
+ ## License
137
+ Released under the [MIT License](http://opensource.org/licenses/MIT).
138
+ Copyright (c) 2015-2016 Ministry of Justice.
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
2
+ load 'rails/tasks/engine.rake'
3
+ load 'rails/tasks/statistics.rake'
4
+
5
+ require 'bundler/gem_tasks'
6
+ require 'rspec/core/rake_task'
7
+
8
+ # Get rid of rspec background noise.
9
+ task(:spec).clear
10
+ RSpec::Core::RakeTask.new(:spec) do |t|
11
+ t.verbose = false
12
+ end
13
+ task default: :spec
14
+
15
+ load 'tasks/mutant.rake'
16
+ load 'tasks/rubocop.rake'
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "govuk/pay/api/client"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/circle.yml ADDED
@@ -0,0 +1,7 @@
1
+ database:
2
+ override:
3
+ - bundle exec rake db:setup
4
+
5
+ test:
6
+ override:
7
+ - bundle exec rake
@@ -0,0 +1,33 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'govuk_pay_api_client/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'govuk-pay-api-client'
8
+ spec.version = GovukPayApiClient::VERSION
9
+ spec.authors = ['Todd Tyree']
10
+ spec.email = ['tatyree@gmail.com']
11
+
12
+ spec.summary = %q{An API client to handle Govuk Pay interactions}
13
+ spec.homepage = 'https://github.com/ministryofjustice/govuk-pay-api-client/'
14
+ spec.licenses = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = 'exe'
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_development_dependency 'bundler', '~> 1.12'
22
+ spec.add_development_dependency 'capybara', '~> 2.7'
23
+ spec.add_development_dependency 'fuubar', '~> 2.0'
24
+ spec.add_development_dependency 'mutant-rspec', '~> 0.8'
25
+ spec.add_development_dependency 'pry-byebug', '~> 3.4'
26
+ spec.add_development_dependency 'rails', '~> 5.0'
27
+ spec.add_development_dependency 'rake', '~> 10.0'
28
+ spec.add_development_dependency 'rspec-rails', '~> 3.5'
29
+ spec.add_development_dependency 'rubocop', '~> 0.41'
30
+ spec.add_development_dependency 'sqlite3', '~> 1.3'
31
+
32
+ spec.add_dependency 'excon', '~> 0.51'
33
+ end
@@ -0,0 +1,61 @@
1
+ module GovukPayApiClient
2
+ module Api
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ end
6
+
7
+ module ClassMethods
8
+ def call(*args)
9
+ new(*args).call
10
+ end
11
+ end
12
+
13
+ def post
14
+ @post ||=
15
+ client.post(path: endpoint, body: request_body.to_json).tap { |resp|
16
+ # Only timeouts and network issues raise errors.
17
+ handle_response_errors(resp)
18
+ @body = resp.body
19
+ }
20
+ rescue Excon::Error => e
21
+ raise Unavailable, e
22
+ end
23
+
24
+ def get
25
+ @get ||=
26
+ client.get(path: endpoint).tap { |resp|
27
+ # Only timeouts and network issues raise errors.
28
+ handle_response_errors(resp)
29
+ @body = resp.body
30
+ }
31
+ rescue Excon::Error => e
32
+ raise Unavailable, e
33
+ end
34
+
35
+ def response_body
36
+ @response_body ||= JSON.parse(@body, symbolize_names: true)
37
+ rescue JSON::ParserError
38
+ ''
39
+ end
40
+
41
+ private
42
+
43
+ def handle_response_errors(resp)
44
+ if (400..599).cover?(resp.status)
45
+ raise Unavailable, resp.status
46
+ end
47
+ end
48
+
49
+ def client
50
+ @client ||= Excon.new(
51
+ ENV.fetch('GOVUK_PAY_API_URL'),
52
+ headers: {
53
+ 'Authorization' => "Bearer #{ENV.fetch('GOVUK_PAY_API_KEY')}",
54
+ 'Content-Type' => 'application/json',
55
+ 'Accept' => 'application/json'
56
+ },
57
+ persistent: true
58
+ )
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,39 @@
1
+ module GovukPayApiClient
2
+ class CreatePayment
3
+ include GovukPayApiClient::Api
4
+ attr_accessor :fee, :return_url
5
+
6
+ def initialize(fee, return_url)
7
+ raise RequiresFeeObject if fee.blank?
8
+ raise RequiresReturnUrl if return_url.blank?
9
+ @fee = fee
10
+ @return_url = return_url
11
+ end
12
+
13
+ def call
14
+ post
15
+ parsed_response
16
+ end
17
+
18
+ private
19
+
20
+ def endpoint
21
+ '/payments'
22
+ end
23
+
24
+ def parsed_response
25
+ OpenStruct.new(
26
+ next_url: response_body.fetch(:_links).fetch(:next_url).fetch(:href)
27
+ )
28
+ end
29
+
30
+ def request_body
31
+ {
32
+ return_url: return_url,
33
+ description: fee.description,
34
+ reference: fee.govpay_reference,
35
+ amount: fee.amount
36
+ }
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,28 @@
1
+ module GovukPayApiClient
2
+ class GetStatus
3
+ include GovukPayApiClient::Api
4
+ attr_accessor :fee
5
+
6
+ def initialize(fee = nil)
7
+ raise RequiresFeeObject if fee.blank?
8
+ @fee = fee
9
+ end
10
+
11
+ def call
12
+ get
13
+ parsed_response
14
+ end
15
+
16
+ private
17
+
18
+ def parsed_response
19
+ OpenStruct.new(
20
+ status: response_body.fetch(:state).fetch(:status)
21
+ )
22
+ end
23
+
24
+ def endpoint
25
+ "/payments/#{fee.govpay_payment_id}"
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,3 @@
1
+ module GovukPayApiClient
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,10 @@
1
+ require 'govuk_pay_api_client/version'
2
+ require 'govuk_pay_api_client/api'
3
+ require 'govuk_pay_api_client/create_payment'
4
+ require 'govuk_pay_api_client/get_status'
5
+
6
+ module GovukPayApiClient
7
+ class Unavailable < StandardError; end
8
+ class RequiresFeeObject < StandardError; end
9
+ class RequiresReturnUrl < StandardError; end
10
+ end
@@ -0,0 +1,10 @@
1
+ task :mutant do
2
+ vars = 'NOCOVERAGE=true'
3
+ flags = '--include lib --use rspec --fail-fast'
4
+ unless system("#{vars} mutant #{flags} GovukPayApiClient*")
5
+ raise 'Mutation testing failed'
6
+ end
7
+ end
8
+
9
+ task(:default).prerequisites << task(:mutant)
10
+
@@ -0,0 +1,6 @@
1
+ if Gem.loaded_specs.key?('rubocop')
2
+ require 'rubocop/rake_task'
3
+ RuboCop::RakeTask.new
4
+
5
+ task(:default).prerequisites << task(:rubocop)
6
+ end
@@ -0,0 +1,11 @@
1
+ namespace :govuk_pay_api_client do
2
+ desc 'Copy the shared examples for govuk pay api rspec testing'
3
+ task :install_shared_examples
4
+ source = File.join(
5
+ Gem.loaded_specs['govuk-pay-api-client'].full_gem_path,
6
+ 'shared_examples',
7
+ 'shared_examples_for_govpay.rb'
8
+ )
9
+ target = File.join(Rails.root, 'spec', 'support', 'shared_examples_for_govpay.rb')
10
+ FileUtils.cp_r source, target
11
+ end
@@ -0,0 +1,188 @@
1
+ module GovpayExample
2
+ module Mocks
3
+ def glimr_api_call
4
+ class_double(
5
+ Excon,
6
+ 'glimr availability check',
7
+ post: instance_double(
8
+ Excon::Response,
9
+ status: 200,
10
+ body: { glimrAvailable: 'yes' }.to_json
11
+ )
12
+ )
13
+ end
14
+
15
+ def a_create_payment_success(initial_payment_response)
16
+ class_double(
17
+ Excon,
18
+ 'glimr availability check',
19
+ post: instance_double(
20
+ Excon::Response,
21
+ status: 200,
22
+ body: initial_payment_response
23
+ )
24
+ )
25
+ end
26
+
27
+ def a_create_payment_timeout
28
+ class_double(Excon, 'create payment timeout').tap { |ex|
29
+ expect(ex).to receive(:post).and_raise(Excon::Errors::Timeout)
30
+ }
31
+ end
32
+
33
+ def a_payment_status_timeout
34
+ class_double(Excon, 'create payment timeout').tap { |ex|
35
+ expect(ex).to receive(:get).and_raise(Excon::Errors::Timeout)
36
+ }
37
+ end
38
+ end
39
+
40
+ module Responses
41
+ def initial_payment_response(payment_id = 'rmpaurrjuehgpvtqg997bt50f')
42
+ {
43
+ 'payment_id' => payment_id,
44
+ 'payment_provider' => 'sandbox',
45
+ 'amount' => 2000,
46
+ 'state' => {
47
+ 'status' => 'created',
48
+ 'finished' => false
49
+ },
50
+ 'description' => 'TC/2016/00001 - Lodgement Fee',
51
+ 'return_url' => 'https://www-integration-2.pymnt.uk/liabilities/960eb61a-a592-4e79-a5f8-c35cde24352a/post_pay',
52
+ 'reference' => '7G20160718180649',
53
+ 'created_date' => '2016-07-18T17:06:53.172Z',
54
+ '_links' => {
55
+ 'self' => {
56
+ 'href' => 'https://publicapi.pymnt.uk/v1/payments/rmpaurrjuehgpvtqg997bt50fm',
57
+ 'method' => 'GET'
58
+ },
59
+ 'next_url' => {
60
+ 'href' => 'https://www-integration-2.pymnt.uk/secure/94b35000-37f2-44e6-a2f5-c0193ca1e98a',
61
+ 'method' => 'GET'
62
+ }
63
+ }
64
+ }.to_json
65
+ end
66
+
67
+ def post_pay_response
68
+ {
69
+ 'payment_id' => 'oio28jhr7mj6rqc9g12pff2i44',
70
+ 'payment_provider' => 'sandbox', 'amount' => 2000,
71
+ 'state' => {
72
+ 'status' => 'success',
73
+ 'finished' => true
74
+ },
75
+ 'description' => 'TC/2016/00001 - Lodgement Fee',
76
+ 'return_url' => 'https://www-integration-2.pymnt.uk/liabilities/7f475fde-b509-4612-bffb-e2dac0066f4c/post_pay',
77
+ 'reference' => '7G20160725115358',
78
+ 'created_date' => '2016-07-25T10:54:00.294Z',
79
+ '_links' => {
80
+ 'self' => {
81
+ 'href' => 'https://publicapi.pymnt.uk/v1/payments/oio28jhr7mj6rqc9g12pff2i44',
82
+ 'method' => 'GET'
83
+ },
84
+ 'next_url' => nil,
85
+ 'next_url_post' => nil,
86
+ 'events' => {
87
+ 'href' => 'https://publicapi.pymnt.uk/v1/payments/oio28jhr7mj6rqc9g12pff2i44/events',
88
+ 'method' => 'GET'
89
+ },
90
+ 'cancel' => nil
91
+ }
92
+ }.to_json
93
+ end
94
+ end
95
+ end
96
+
97
+ RSpec.shared_examples 'govpay payment response' do |fee, govpay_payment_id|
98
+ include GovpayExample::Responses
99
+
100
+ let(:request_body) {
101
+ {
102
+ return_url: 'the_return_url',
103
+ description: fee.description,
104
+ reference: fee.govpay_reference,
105
+ amount: fee.amount
106
+ }.to_json
107
+ }
108
+
109
+ before do
110
+ Excon.stub(
111
+ {
112
+ method: :post,
113
+ host: 'govpay-test.dsd.io',
114
+ body: request_body,
115
+ path: '/payments'
116
+ },
117
+ status: 201, body: initial_payment_response(govpay_payment_id)
118
+ )
119
+
120
+ Excon.stub(
121
+ {
122
+ method: :get,
123
+ host: 'govpay-test.dsd.io',
124
+ path: "/payments/#{govpay_payment_id}"
125
+ },
126
+ status: 200, body: post_pay_response
127
+ )
128
+ end
129
+ end
130
+
131
+ RSpec.shared_examples 'govpay returns a 404' do |fee|
132
+
133
+ let(:request_body) {
134
+ {
135
+ return_url: 'the_return_url',
136
+ description: fee.description,
137
+ reference: fee.govpay_reference,
138
+ amount: fee.amount
139
+ }.to_json
140
+ }
141
+
142
+ before do
143
+ Excon.stub(
144
+ {
145
+ method: :post,
146
+ host: 'govpay-test.dsd.io',
147
+ body: request_body,
148
+ path: '/payments'
149
+ },
150
+ status: 404
151
+ )
152
+ end
153
+ end
154
+
155
+ RSpec.shared_examples 'govpay post_pay returns a 500' do |govpay_payment_id|
156
+ before do
157
+ Excon.stub(
158
+ {
159
+ method: :get,
160
+ host: 'govpay-test.dsd.io',
161
+ path: "/payments/#{govpay_payment_id}"
162
+ },
163
+ status: 500, body: '{"message":"Govpay is not working"}'
164
+ )
165
+ end
166
+ end
167
+
168
+ RSpec.shared_examples 'govpay payment status times out' do
169
+ include GovpayExample::Responses
170
+ include GovpayExample::Mocks
171
+
172
+ before do
173
+ expect(Excon).to receive(:new).
174
+ and_return(
175
+ a_payment_status_timeout
176
+ )
177
+ end
178
+ end
179
+
180
+ RSpec.shared_examples 'govpay create payment times out' do
181
+ include GovpayExample::Responses
182
+ include GovpayExample::Mocks
183
+
184
+ before do
185
+ expect(Excon).to receive(:new).
186
+ and_return(a_create_payment_timeout)
187
+ end
188
+ end