govuk-pay-api-client 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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