paypoint-blue 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9097aafc20b17695e989cce94618d414cb72d404
4
+ data.tar.gz: 7dfc8e09ba1b8268dfc7e357eb62b87ca8964d86
5
+ SHA512:
6
+ metadata.gz: fa0c78e2028e56f6b876ba359a0090360975fee9432d6e87af6420d022069af5f01ce607d5d7f3bc4b46afad7a72bba970d5047b1224f7acd0cb5ac5de4c30f6
7
+ data.tar.gz: 9ae0701c9674f7b3b048f42d23ab68de6417bf14c11c8ebef486d220a46991115bebc961a90d449be04ece80f4ddd5ea1213b6fee58c469c1873072e53f538c0
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.1
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ -e support/yard_ext.rb --tag "applies_defaults:Payload defaults this method will apply" --tag "api_url:API reference"
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in paypoint-blue.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Laszlo Bacsi
4
+ Copyright (c) 2015 Drop and Collect Ltd
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in
14
+ all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,71 @@
1
+ # PayPoint::Blue
2
+
3
+ API client for PayPoint's 3rd generation PSP product a.k.a PayPoint Blue.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'paypoint-blue'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install paypoint-blue
18
+
19
+ ## Usage
20
+
21
+ Read the [documentation](http://www.rubydoc.info/gems/paypoint-blue).
22
+
23
+ Run `bin/console` to start an interactive prompt for a playgound where
24
+ you can experiment with the API. You will have a bunch of meaningful
25
+ defaults set and some helpers to use. Just call the `help` or `h` method
26
+ in the console to learn more about the different helpers.
27
+
28
+ ### Example
29
+
30
+ # Endpoint can be the actual URL or one of :test or :live.
31
+ # Installation id and credentials default to these ENV vars if omitted.
32
+ blue = PayPoint::Blue.hosted_client(
33
+ endpoint: :test,
34
+ inst_id: ENV['BLUE_API_INSTALLATION'],
35
+ api_id: ENV['BLUE_API_ID'],
36
+ api_password: ENV['BLUE_API_PASSWORD'],
37
+ defaults: {
38
+ currency: "GBP",
39
+ return_url: "http://example.com/callback/return",
40
+ skin: "9001"
41
+ }
42
+ )
43
+
44
+ blue.ping # => true
45
+
46
+ result = blue.make_payment(
47
+ merchant_ref: "abcd-1234",
48
+ amount: "4.89",
49
+ customer_ref: "42",
50
+ customer_name: "Alice"
51
+ )
52
+ result.session_id # => "39ac..."
53
+ result.redirect_url # => "https://hosted.paypoint.net/..."
54
+
55
+ # The hosted product doesn't have this endpoint, but the client will delegate
56
+ # this request to an API client for the regular API product behind the scenes.
57
+ blue.transaction(transaction_id) # => { processing: { ... }, payment_method: { ... }, ... }
58
+
59
+ ## Development
60
+
61
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment.
62
+
63
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release` to create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
64
+
65
+ ## Contributing
66
+
67
+ 1. Fork it ( https://github.com/CPlus/paypoint-blue/fork )
68
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
69
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
70
+ 4. Push to the branch (`git push origin my-new-feature`)
71
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "rake/testtask"
2
+ require "bundler/gem_tasks"
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs << "test"
6
+ t.test_files = FileList['test/test_*.rb']
7
+ t.verbose = true
8
+ end
data/bin/console ADDED
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "paypoint/blue"
5
+
6
+ def bold(text)
7
+ "\e[1m#{text}\e[0m"
8
+ end
9
+
10
+ def help
11
+ puts <<-EOS
12
+ #{bold "API client helpers"}
13
+
14
+ #{bold "blue"}(**api_options)
15
+ #{bold "blue_hosted"}(**api_options)
16
+
17
+ Create and memoize API clients with these two. Options are the same as for
18
+ PayPoint::Blue::Base#initialize, but default endpoint to :test.
19
+
20
+ Export BLUE_API_INSTALLATION, BLUE_API_ID, and BLUE_API_PASSWORD as environment
21
+ variables, and you won't have to pass them to the helpers. See direnv.net for
22
+ an easy way to do that.
23
+
24
+ It's recommended to use either logging or the Runscope integration to keep track
25
+ and inspect all the traffic. You may set the RUNSCOPE_BUCKET environment
26
+ variable to have the integration be turned on by default. Otherwise you only
27
+ have to provide the runscope option the first time you call one of the API
28
+ client helpers:
29
+
30
+ blue(runscope: 'bucket-key')
31
+
32
+ #{bold "Callback endpoints"}
33
+
34
+ #{bold "cb"}(id)
35
+
36
+ This is most useful when using the Runscope integration. The available ids are:
37
+
38
+ :preauth_proceed, :preauth_cancel, :preauth_suspend, :preauth_suspend_replay,
39
+ :postauth_proceed, :postauth_cancel, :empty
40
+
41
+ These will return mocky.io urls which return the proper response. Using the
42
+ Runscope integration you will see the requests PayPoint makes to these endpoints
43
+ in your bucket.
44
+
45
+ #{bold "callbacks"}()
46
+
47
+ Returns the callbacks section of the payload with all callbacks set to proceed.
48
+
49
+ #{bold "Credit/Debit cards helpers"}
50
+
51
+ #{bold "card"}(type: :mc, valid: true, threeDS: true)
52
+
53
+ Returns a credit/debit card number for testing. Possible options:
54
+
55
+ type: :mc_debit, :mc_credit, :visa_debit, :visa_credit
56
+ valid: true, false
57
+ threeDS: true, false, :unknown
58
+ EOS
59
+ end
60
+ alias :h :help
61
+
62
+ def blue_default_options
63
+ return_base = ENV['RUNSCOPE_BUCKET'] ? "https://#{RUNSCOPE_BUCKET}.runscope.net" : "http://bluedemo.dev"
64
+ {
65
+ runscope: ENV['RUNSCOPE_BUCKET'],
66
+ defaults: {
67
+ currency: 'GBP',
68
+ commerce_type: 'ECOM',
69
+ skin: ENV['BLUE_SKIN'],
70
+ return_url: "#{return_base}/callback/return",
71
+ restore_url: "#{return_base}/callback/restore",
72
+ pre_auth_callback: cb(:preauth_proceed),
73
+ post_auth_callback: cb(:postauth_proceed),
74
+ transaction_notification: cb(:empty),
75
+ expiry_notification: cb(:empty)
76
+ }
77
+ }
78
+ end
79
+
80
+ def blue(endpoint: :test, **options)
81
+ options = blue_default_options.merge(options)
82
+ $blue_api ||= PayPoint::Blue.api_client(endpoint: endpoint, **options)
83
+ end
84
+
85
+ def blue_hosted(endpoint: :test, **options)
86
+ options = blue_default_options.merge(options)
87
+ $blue_hosted ||= PayPoint::Blue.hosted_client(endpoint: endpoint, **options)
88
+ end
89
+
90
+ def cb(id)
91
+ {
92
+ preauth_proceed: "http://www.mocky.io/v2/550f10df3645066a0a2a420e",
93
+ preauth_cancel: "http://www.mocky.io/v2/550f10ec364506660a2a420f",
94
+ preauth_suspend: "http://www.mocky.io/v2/550f10ff364506670a2a4210",
95
+ preauth_suspend_replay: "http://www.mocky.io/v2/550f110a364506690a2a4211",
96
+ postauth_proceed: "http://www.mocky.io/v2/550f1159364506650a2a4212",
97
+ postauth_cancel: "http://www.mocky.io/v2/550f11633645066c0a2a4213",
98
+ empty: "http://www.mocky.io/v2/550f11723645066c0a2a4214"
99
+ }[id]
100
+ end
101
+
102
+ def callbacks
103
+ {
104
+ callbacks: {
105
+ preAuthCallback: { url: cb(:preauth_proceed), format: "REST_JSON" },
106
+ postAuthCallback: { url: cb(:postauth_proceed), format: "REST_JSON" },
107
+ transactionNotification: { url: cb(:empty), format: "REST_JSON" },
108
+ expiryNotification: { url: cb(:empty), format: "REST_JSON" }
109
+ }
110
+ }
111
+ end
112
+
113
+ def card(type: :mc_debit, valid: true, threeDS: true)
114
+ {
115
+ mc_debit: {
116
+ true => {
117
+ true => "9900000000005159",
118
+ false => "9900000000000010",
119
+ :unknown => "9900000000010258"
120
+ },
121
+ false => {
122
+ true => "9900000000005282",
123
+ false => "9900000000000168",
124
+ :unknown => "9900000000010407"
125
+ }
126
+ },
127
+ mc_credit: {
128
+ true => {
129
+ true => "9901000000005133",
130
+ false => "9901000000000019",
131
+ :unknown => "9901000000010257"
132
+ },
133
+ false => {
134
+ true => "9901000000005281",
135
+ false => "9901000000000167",
136
+ :unknown => "9901000000010406"
137
+ }
138
+ },
139
+ visa_debit: {
140
+ true => {
141
+ true => "9902000000005132",
142
+ false => "9902000000000018",
143
+ :unknown => "9902000000010256"
144
+ },
145
+ false => {
146
+ true => "9902000000005280",
147
+ false => "9902000000000166",
148
+ :unknown => "9902000000010405"
149
+ }
150
+ },
151
+ visa_credit: {
152
+ true => {
153
+ true => "9903000000005131",
154
+ false => "9903000000000017",
155
+ :unknown => "9903000000010255"
156
+ },
157
+ false => {
158
+ true => "9903000000005289",
159
+ false => "9903000000000165",
160
+ :unknown => "9903000000010404"
161
+ }
162
+ }
163
+ }[type][valid][threeDS]
164
+ end
165
+
166
+ require "irb"
167
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,134 @@
1
+ require "paypoint/blue/base"
2
+
3
+ # Client class for the API product.
4
+ class PayPoint::Blue::API < PayPoint::Blue::Base
5
+
6
+ ENDPOINTS = {
7
+ test: "https://api.mite.paypoint.net:2443/acceptor/rest",
8
+ live: "https://api.paypoint.net/acceptor/rest",
9
+ }.freeze
10
+
11
+ shortcut :merchant_ref, 'transaction.merchant_ref'
12
+ shortcut :amount, 'transaction.amount'
13
+ shortcut :currency, 'transaction.currency'
14
+ shortcut :commerce_type, 'transaction.commerce_type'
15
+ shortcut :customer_ref, 'customer.merchant_ref'
16
+ shortcut :customer_name, 'customer.display_name'
17
+
18
+ shortcut :pre_auth_callback, 'callbacks.pre_auth_callback.url'
19
+ shortcut :post_auth_callback, 'callbacks.post_auth_callback.url'
20
+ shortcut :transaction_notification, 'callbacks.transaction_notification.url'
21
+ shortcut :expiry_notification, 'callbacks.expiry_notification.url'
22
+
23
+ # Test connectivity
24
+ #
25
+ # @return [true,false]
26
+ def ping
27
+ client.get "transactions/ping"
28
+ true
29
+ rescue Faraday::ClientError
30
+ false
31
+ end
32
+
33
+ # Make a payment
34
+ #
35
+ # @api_url https://developer.paypoint.com/payments/docs/#payments/make_a_payment
36
+ #
37
+ # @applies_defaults
38
+ # +:currency+, +:commerce_type+, +:pre_auth_callback+,
39
+ # +:post_auth_callback+, +:transaction_notification+,
40
+ # +:expiry_notification+
41
+ #
42
+ # @param [Hash] payload the payload is made up of the keyword
43
+ # arguments passed to the method
44
+ #
45
+ # @return the API response
46
+ def make_payment(**payload)
47
+ payload = build_payload(payload,
48
+ defaults: %i[
49
+ currency commerce_type pre_auth_callback post_auth_callback
50
+ transaction_notification expiry_notification
51
+ ]
52
+ )
53
+ client.post "transactions/#{inst_id}/payment", payload
54
+ end
55
+
56
+ # Submit an authorisation
57
+ #
58
+ # @api_url https://developer.paypoint.com/payments/docs/#payments/submit_an_authorisation
59
+ # @see #make_payment
60
+ #
61
+ # This is a convenience method which makes a payment with the
62
+ # transaction's +deferred+ value set to +true+.
63
+ #
64
+ # @param (see #make_payment)
65
+ #
66
+ # @return the API response
67
+ def submit_authorisation(**payload)
68
+ payload[:transaction] ||= {}
69
+ payload[:transaction][:deferred] = true
70
+ make_payment(**payload)
71
+ end
72
+
73
+ # Capture an authorisation
74
+ #
75
+ # @api_url https://developer.paypoint.com/payments/docs/#payments/capture_an_authorisation
76
+ #
77
+ # @applies_defaults +:commerce_type+
78
+ #
79
+ # @param [String] transaction_id the id of the previous transaction
80
+ # @param [Hash] payload the payload is made up of the keyword
81
+ # arguments passed to the method
82
+ #
83
+ # @return the API response
84
+ def capture_authorisation(transaction_id, **payload)
85
+ payload = build_payload(payload, defaults: %i[commerce_type])
86
+ client.post "transactions/#{inst_id}/#{transaction_id}/capture", payload
87
+ end
88
+
89
+ # Cancel an authorisation
90
+ #
91
+ # @api_url https://developer.paypoint.com/payments/docs/#payments/cancel_an_authorisation
92
+ #
93
+ # @applies_defaults +:commerce_type+
94
+ #
95
+ # @param (see #capture_authorisation)
96
+ #
97
+ # @return the API response
98
+ def cancel_authorisation(transaction_id, **payload)
99
+ payload = build_payload(payload, defaults: %i[commerce_type])
100
+ client.post "transactions/#{inst_id}/#{transaction_id}/cancel", payload
101
+ end
102
+
103
+ # Get transaction details
104
+ #
105
+ # @api_url https://developer.paypoint.com/payments/docs/#payments/request_a_previous_transaction
106
+ #
107
+ # @param [String] transaction_id the id of the transaction
108
+ #
109
+ # @return the API response
110
+ def transaction(transaction_id)
111
+ client.get "transactions/#{inst_id}/#{transaction_id}"
112
+ end
113
+
114
+ # Refund a payment
115
+ #
116
+ # @api_url https://developer.paypoint.com/payments/docs/#payments/refund_a_payment
117
+ #
118
+ # Without a payload this will refund the full amount. If you only want
119
+ # to refund a smaller amount, you will need to pass either the
120
+ # +amount+ or a +transaction+ hash as a keyword argument.
121
+ #
122
+ # @example Partial refund
123
+ # blue.refund_payment(txn_id, amount: '3.49') # assumes currency set as default
124
+ #
125
+ # @param (see #capture_authorisation)
126
+ #
127
+ # @return the API response
128
+ def refund_payment(transaction_id, **payload)
129
+ defaults = payload[:amount] || payload[:transaction] && payload[:transaction][:amount] ? %i[currency commerce_type] : []
130
+ payload = build_payload(payload, defaults: defaults)
131
+ client.post "transactions/#{inst_id}/#{transaction_id}/refund", payload
132
+ end
133
+
134
+ end
@@ -0,0 +1,122 @@
1
+ require "faraday"
2
+ require "faraday_middleware"
3
+
4
+ require "paypoint/blue/payload_builder"
5
+ require "paypoint/blue/body_extractor"
6
+ require "paypoint/blue/hash_key_converter"
7
+ require "paypoint/blue/raise_errors"
8
+ require "paypoint/blue/faraday_runscope"
9
+
10
+ module PayPoint
11
+ module Blue
12
+
13
+ # Abstract base class for the API clients. Takes care of
14
+ # initializing the Faraday client by setting up middlewares in the
15
+ # right order.
16
+ #
17
+ # The base class doesn't implement any of the API interface methods,
18
+ # but leaves that to the subclasses.
19
+ #
20
+ # @note All payloads expected by the API interface methods in the
21
+ # subclasses will be processed by {PayloadBuilder} and the
22
+ # {HashKeyConverter} middleware. Shortcuts will be moved to their
23
+ # proper paths, default values will be applied, and snakecase keys
24
+ # will be converted to camelcase.
25
+ #
26
+ # @abstract
27
+ class Base
28
+
29
+ include PayloadBuilder
30
+
31
+ # The Faraday client
32
+ attr_reader :client
33
+
34
+ # Creates a PayPoint Blue API client
35
+ #
36
+ # Options not listed here will be passed on to the Faraday client.
37
+ #
38
+ # @param endpoint +:test+, +:live+, or a string with the API
39
+ # endpoint URL
40
+ # @param inst_id the ID for your installation as provided by
41
+ # PayPoint
42
+ # @param api_id your API user ID
43
+ # @param api_password your API user password
44
+ #
45
+ # @option options [Hash] :defaults default payload values; see the
46
+ # documentation of the methods of {API} and {Hosted} for the
47
+ # payload keys that can have defaults
48
+ # @option options [true,false] :log whether to log requests and
49
+ # responses
50
+ # @option options [Logger] :logger a custom logger instance,
51
+ # implies +log: true+
52
+ # @option options [true,false] :raw whether to return the raw
53
+ # +Faraday::Response+ object instead of a parsed value
54
+ # @option options [String] :runscope when used, all traffic will
55
+ # pass through the provided {https://www.runscope.com/ Runscope}
56
+ # bucket, including notification callbacks
57
+ def initialize(endpoint:,
58
+ inst_id: ENV['BLUE_API_INSTALLATION'],
59
+ api_id: ENV['BLUE_API_ID'],
60
+ api_password: ENV['BLUE_API_PASSWORD'],
61
+ **options)
62
+
63
+ @endpoint = self.class.const_get('ENDPOINTS').fetch(endpoint, endpoint.to_s)
64
+
65
+ @inst_id = inst_id or raise ArgumentError, "missing inst_id"
66
+ @api_id = api_id or raise ArgumentError, "missing api_id"
67
+ @api_password = api_password or raise ArgumentError, "missing api_password"
68
+
69
+ options[:url] = @endpoint
70
+ @options = options
71
+
72
+ self.defaults = options.delete(:defaults)
73
+
74
+ @client = build_client
75
+ end
76
+
77
+ private
78
+
79
+ attr_reader :inst_id, :options
80
+
81
+ def client_options
82
+ options.select { |k,v| Faraday::ConnectionOptions.members.include?(k) }
83
+ end
84
+
85
+ def build_client
86
+ Faraday.new(client_options) do |f|
87
+ unless options[:raw]
88
+ # This extracts the body and discards all other data from the
89
+ # Faraday::Response object. It should be placed here before
90
+ # all response middlewares, so that it runs as the last one.
91
+ f.use PayPoint::Blue::BodyExtractor
92
+ end
93
+
94
+ f.use PayPoint::Blue::RaiseErrors
95
+ unless options[:raw]
96
+ f.response :mashify
97
+ f.use PayPoint::Blue::HashKeyConverter
98
+ end
99
+ f.response :dates
100
+ f.response :json, content_type: /\bjson$/
101
+ f.response :logger, options[:logger] if options[:logger] || options[:log]
102
+
103
+ # This sends all API traffic through Runscope, including
104
+ # notifications. It needs to be inserted here before the JSON
105
+ # request middleware so that it is able to transform
106
+ # notification URLs too.
107
+ if options[:runscope]
108
+ f.use FaradayRunscope, options[:runscope],
109
+ transform_paths: /\A(callbacks|session)\.\w+(Callback|Notification)\.url\Z/
110
+ end
111
+
112
+ f.request :basic_auth, @api_id, @api_password
113
+ f.request :json
114
+
115
+ f.adapter Faraday.default_adapter
116
+ end
117
+ end
118
+
119
+ end
120
+
121
+ end
122
+ end
@@ -0,0 +1,18 @@
1
+ module PayPoint
2
+ module Blue
3
+
4
+ # Faraday middleware which extracts the body from the response
5
+ # object and returns with just that discarding all other meta
6
+ # information.
7
+ class BodyExtractor < Faraday::Middleware
8
+
9
+ # Extract and return just the body discarding everything else
10
+ def call(env)
11
+ response = @app.call(env)
12
+ response.env[:body]
13
+ end
14
+
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,57 @@
1
+ module PayPoint
2
+ module Blue
3
+
4
+ # Abstract error base class
5
+ # @abstract
6
+ class Error < StandardError
7
+
8
+ # the response that caused the error
9
+ attr_reader :response
10
+
11
+ # the outcome code (e.g. +'V402'+)
12
+ attr_reader :code
13
+
14
+ # Initializes the error from the response object. It uses the
15
+ # outcome message from the response if set.
16
+ def initialize(response)
17
+ @response = response
18
+
19
+ if outcome
20
+ @code = outcome[:reason_code]
21
+ message = outcome[:reason_message]
22
+ else
23
+ message = "the server responded with status #{response[:status]}"
24
+ end
25
+
26
+ super(message)
27
+ end
28
+
29
+ private
30
+
31
+ def outcome
32
+ @outcome ||= response[:body].is_a?(Hash) && response[:body][:outcome]
33
+ end
34
+
35
+ # Generic client error class, also a base class for more specific
36
+ # types of errors
37
+ class Client < Error; end
38
+
39
+ # Specific error class for errors with a +'V'+ outcome code
40
+ class Validation < Error; end
41
+
42
+ # Specific error class for errors with an +'A'+ outcome code
43
+ class Auth < Error; end
44
+
45
+ # Specific error class for errors with a +'C'+ outcome code
46
+ class Cancelled < Error; end
47
+
48
+ # Specific error class for errors with a +'X'+ outcome code
49
+ class External < Error; end
50
+
51
+ # Specific error class for errors with an +'U'+ outcome code
52
+ class Suspended < Error; end
53
+
54
+ end
55
+
56
+ end
57
+ end
@@ -0,0 +1,74 @@
1
+ class FaradayRunscope < Faraday::Middleware
2
+
3
+ CUSTOM_PORT = "Runscope-Request-Port".freeze
4
+
5
+ def initialize(app, bucket, transform_paths: false)
6
+ super(app)
7
+ self.bucket = bucket
8
+ self.transform_paths = Array(transform_paths)
9
+ end
10
+
11
+ def call(env)
12
+ if env.url.port != env.url.default_port
13
+ env.request_headers[CUSTOM_PORT] = env.url.port.to_s
14
+ env.url.port = env.url.default_port
15
+ end
16
+
17
+ transform_url env.url
18
+
19
+ if transform_paths && env.body.respond_to?(:each_with_index)
20
+ transform_paths!(env.body)
21
+ end
22
+
23
+ @app.call env
24
+ end
25
+
26
+ protected
27
+
28
+ attr_accessor :bucket, :transform_paths
29
+
30
+ def transform_url(url)
31
+ if url.respond_to?(:host=)
32
+ url.host = runscope_host(url.host)
33
+ elsif url.is_a?(String)
34
+ uri = URI.parse(url)
35
+ uri.host = runscope_host(uri.host)
36
+ return uri.to_s
37
+ end
38
+ url
39
+ end
40
+
41
+ def runscope_host(host)
42
+ "#{host.tr('.', '-')}-#{bucket}.runscope.net"
43
+ end
44
+
45
+ def transform_paths!(enum, path=nil)
46
+ each_pair(enum) do |key, value|
47
+ key_path = path ? "#{path}.#{key}" : key.to_s
48
+ if value.respond_to?(:each_with_index)
49
+ transform_paths!(value, key_path)
50
+ elsif transform_path?(key_path)
51
+ enum[key] = transform_url(value)
52
+ end
53
+ end
54
+ end
55
+
56
+ def each_pair(enum)
57
+ if enum.respond_to?(:each_pair)
58
+ enum.each_pair do |key, value|
59
+ yield key, value
60
+ end
61
+ else
62
+ enum.each_with_index do |value, index|
63
+ yield index, value
64
+ end
65
+ end
66
+ end
67
+
68
+ def transform_path?(path)
69
+ transform_paths.any? do |path_to_transform|
70
+ path_to_transform === path
71
+ end
72
+ end
73
+
74
+ end
@@ -0,0 +1,26 @@
1
+ module PayPoint
2
+ module Blue
3
+
4
+ # Faraday middleware for converting hash keys in the request payload
5
+ # from snake_case to camelCase and the other way around in the
6
+ # response.
7
+ class HashKeyConverter < Faraday::Middleware
8
+
9
+ # Convert hash keys to camelCase in the request and to snake_case
10
+ # in the response
11
+ def call(env)
12
+ if env.body.is_a?(Enumerable)
13
+ env.body = Utils.camelcase_and_symbolize_keys(env.body)
14
+ end
15
+
16
+ @app.call(env).on_complete do |response_env|
17
+ if response_env.body.is_a?(Enumerable)
18
+ response_env.body = Utils.snakecase_and_symbolize_keys(response_env.body)
19
+ end
20
+ end
21
+ end
22
+
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,92 @@
1
+ require "forwardable"
2
+
3
+ require "paypoint/blue/base"
4
+
5
+ # Client class for the Hosted product.
6
+ class PayPoint::Blue::Hosted < PayPoint::Blue::Base
7
+
8
+ ENDPOINTS = {
9
+ test: "https://hosted.mite.paypoint.net/hosted/rest",
10
+ live: "https://hosted.paypoint.net/hosted/rest"
11
+ }.freeze
12
+
13
+ shortcut :merchant_ref, 'transaction.merchant_reference'
14
+ shortcut :amount, 'transaction.money.amount.fixed'
15
+ shortcut :currency, 'transaction.money.currency'
16
+ shortcut :customer_ref, 'customer.identity.merchant_customer_id'
17
+ shortcut :customer_name, 'customer.details.name'
18
+ shortcut :return_url, 'session.return_url.url'
19
+ shortcut :restore_url, 'session.restore_url.url'
20
+ shortcut :skin, 'session.skin'
21
+
22
+ shortcut :pre_auth_callback, 'session.pre_auth_callback.url'
23
+ shortcut :post_auth_callback, 'session.post_auth_callback.url'
24
+ shortcut :transaction_notification, 'session.transaction_notification.url'
25
+
26
+ extend Forwardable
27
+
28
+ def_delegators :@api_client,
29
+ :capture_authorisation,
30
+ :cancel_authorisation,
31
+ :transaction,
32
+ :refund_payment
33
+
34
+ # The Hosted product has only a few endpoints. However, users most
35
+ # likey will want to access the endpoints of the API product as well.
36
+ # Therefore, this class also delegates to an API client which is
37
+ # initialized using the same options that this object receives.
38
+ def initialize(**options)
39
+ @api_client = PayPoint::Blue::API.new(**options)
40
+ super
41
+ end
42
+
43
+ # Test connectivity
44
+ #
45
+ # @return [true,false]
46
+ def ping
47
+ client.get "sessions/ping"
48
+ true
49
+ rescue Faraday::ClientError
50
+ false
51
+ end
52
+
53
+ # Make a payment
54
+ #
55
+ # @api_url https://developer.paypoint.com/payments/docs/#payments/make_a_payment
56
+ #
57
+ # @applies_defaults
58
+ # +:currency+, +:return_url+, +:restore_url+, +:skin+,
59
+ # +:pre_auth_callback+, +:post_auth_callback+, +:transaction_notification+
60
+ #
61
+ # @param [Hash] payload the payload is made up of the keyword
62
+ # arguments passed to the method
63
+ #
64
+ # @return the API response
65
+ def make_payment(**payload)
66
+ payload = build_payload(payload,
67
+ defaults: %i[
68
+ currency return_url restore_url skin
69
+ pre_auth_callback post_auth_callback transaction_notification
70
+ ]
71
+ )
72
+ client.post "sessions/#{inst_id}/payments", build_payload(payload)
73
+ end
74
+
75
+ # Submit an authorisation
76
+ #
77
+ # @api_url https://developer.paypoint.com/payments/docs/#payments/submit_an_authorisation
78
+ # @see #make_payment
79
+ #
80
+ # This is a convenience method which makes a payment with the
81
+ # transaction's +deferred+ value set to +true+.
82
+ #
83
+ # @param (see #make_payment)
84
+ #
85
+ # @return the API response
86
+ def submit_authorisation(**payload)
87
+ payload[:transaction] ||= {}
88
+ payload[:transaction][:deferred] = true
89
+ make_payment(**payload)
90
+ end
91
+
92
+ end
@@ -0,0 +1,77 @@
1
+ module PayPoint
2
+ module Blue
3
+
4
+ # Provides helper methods for payload construction used throughout
5
+ # the API. It allows definition of payload shortcuts and default
6
+ # values.
7
+ module PayloadBuilder
8
+
9
+ def self.included(base)
10
+ base.extend ClassMethods
11
+ end
12
+
13
+ module ClassMethods
14
+ attr_accessor :shortcuts
15
+
16
+ # Define a payload shortcut
17
+ #
18
+ # Shortcuts help payload construction by defining short aliases
19
+ # to commonly used paths.
20
+ #
21
+ # @example Define and use a shortcut
22
+ # PayPoint::Blue::Hosted.shortcut :amount, 'transaction.money.amount.fixed'
23
+ # blue.make_payment(amount: '3.49', ...)
24
+ # # this will be turned into
25
+ # # { transaction: { money: { amount: { fixed: '3.49' } } } }
26
+ #
27
+ # @param [Symbol] key the shortcut key
28
+ # @param [String] path a path into the payload with segments
29
+ # separated by dots (e.g. +'transaction.money.amount.fixed'+)
30
+ def shortcut(key, path=nil)
31
+ if path.nil?
32
+ shortcuts && shortcuts[key]
33
+ else
34
+ self.shortcuts ||= {}
35
+ shortcuts[key] = path
36
+ end
37
+ end
38
+ end
39
+
40
+ attr_accessor :defaults
41
+
42
+ # Builds the payload by applying default values and replacing
43
+ # shortcuts
44
+ #
45
+ # @param [Hash] payload the original payload using shortcuts
46
+ # @param [Array<Symbol>] defaults an array of symbols for defaults
47
+ # that should be applied to this payload
48
+ def build_payload(payload, defaults: [])
49
+ apply_defaults(payload, defaults)
50
+ payload.keys.each do |key|
51
+ if path = self.class.shortcut(key)
52
+ value = payload.delete(key)
53
+ segments = path.split('.').map(&:to_sym)
54
+ leaf = segments.pop
55
+ leaf_parent = segments.reduce(payload) {|h,k| h[k] ||= {}}
56
+ leaf_parent[leaf] ||= value
57
+ end
58
+ end
59
+ payload
60
+ end
61
+
62
+ private
63
+
64
+ def apply_defaults(payload, applicable_defaults)
65
+ return unless defaults
66
+
67
+ defaults.each do |key, value|
68
+ if applicable_defaults.include?(key) && !payload.has_key?(key)
69
+ payload[key] = value
70
+ end
71
+ end
72
+ end
73
+
74
+ end
75
+
76
+ end
77
+ end
@@ -0,0 +1,53 @@
1
+ require "paypoint/blue/error"
2
+
3
+ module PayPoint
4
+ module Blue
5
+
6
+ # Faraday response middleware for handling various error scenarios
7
+ class RaiseErrors < Faraday::Response::Middleware
8
+
9
+ # Raise an error if the response outcome signifies a failure or
10
+ # the HTTP status code is 400 or greater.
11
+ #
12
+ # @raise [Error::Validation] for an outcome code starting with +V+
13
+ # @raise [Error::Auth] for an outcome code starting with +A+
14
+ # @raise [Error::Cancelled] for an outcome code starting with +C+
15
+ # @raise [Error::External] for an outcome code starting with +X+
16
+ # @raise [Error::Suspended] for an outcome code starting with +U+
17
+ # @raise [Error::Client] for all other error scenarios
18
+ def on_complete(env)
19
+ outcome = fetch_outcome(env)
20
+ if outcome
21
+ case outcome[:reason_code]
22
+ when /^S/ then return
23
+ when /^V/ then raise Error::Validation, response_values(env)
24
+ when /^A/ then raise Error::Auth, response_values(env)
25
+ when /^C/ then raise Error::Cancelled, response_values(env)
26
+ when /^X/ then raise Error::External, response_values(env)
27
+ when /^U/ then raise Error::Suspended, response_values(env)
28
+ else
29
+ raise Error::Client, response_values(env)
30
+ end
31
+ elsif env.status >= 400
32
+ raise Error::Client, response_values(env)
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def fetch_outcome(env)
39
+ env.body.is_a?(Hash) && env.body[:outcome]
40
+ end
41
+
42
+ def response_values(env)
43
+ {
44
+ status: env.status,
45
+ headers: env.response_headers,
46
+ body: env.body
47
+ }
48
+ end
49
+
50
+ end
51
+
52
+ end
53
+ end
@@ -0,0 +1,51 @@
1
+ module PayPoint
2
+ module Blue
3
+ module Utils
4
+
5
+ extend self
6
+
7
+ def snakecase_and_symbolize_keys(hash)
8
+ case hash
9
+ when Hash
10
+ hash.each_with_object({}) do |(key, value), snakified|
11
+ snakified[snakecase(key)] = snakecase_and_symbolize_keys(value)
12
+ end
13
+ when Enumerable
14
+ hash.map {|v| snakecase_and_symbolize_keys(v)}
15
+ else
16
+ hash
17
+ end
18
+ end
19
+
20
+ def camelcase_and_symbolize_keys(hash)
21
+ case hash
22
+ when Hash
23
+ hash.each_with_object({}) do |(key, value), camelized|
24
+ camelized[camelcase(key)] = camelcase_and_symbolize_keys(value)
25
+ end
26
+ when Enumerable
27
+ hash.map {|v| camelcase_and_symbolize_keys(v)}
28
+ else
29
+ hash
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def snakecase(original)
36
+ string = original.is_a?(Symbol) ? original.to_s : original.dup
37
+ string.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
38
+ string.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
39
+ string.downcase!
40
+ string.to_sym
41
+ end
42
+
43
+ def camelcase(original)
44
+ string = original.is_a?(Symbol) ? original.to_s : original.dup
45
+ string.gsub!(/_([a-z\d]*)/) { $1.capitalize }
46
+ string.to_sym
47
+ end
48
+
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,5 @@
1
+ module PayPoint
2
+ module Blue
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,24 @@
1
+ require "paypoint/blue/version"
2
+ require "paypoint/blue/api"
3
+ require "paypoint/blue/hosted"
4
+ require "paypoint/blue/utils"
5
+
6
+ module PayPoint
7
+ module Blue
8
+
9
+ # Creates a client for the PayPoint Blue API product
10
+ #
11
+ # @see PayPoint::Blue::Base#initialize
12
+ def self.api_client(**options)
13
+ PayPoint::Blue::API.new(**options)
14
+ end
15
+
16
+ # Creates a client for the PayPoint Blue Hosted product
17
+ #
18
+ # @see PayPoint::Blue::Base#initialize
19
+ def self.hosted_client(**options)
20
+ PayPoint::Blue::Hosted.new(**options)
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'paypoint/blue/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "paypoint-blue"
8
+ spec.version = PayPoint::Blue::VERSION
9
+ spec.authors = ["Laszlo Bacsi"]
10
+ spec.email = ["lackac@lackac.hu"]
11
+
12
+ spec.summary = %q{API client for PayPoint Blue}
13
+ spec.description = %q{API client for PayPoint's 3rd generation PSP product a.k.a PayPoint Blue}
14
+ spec.homepage = "https://github.com/CPlus/paypoint-blue"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_dependency "faraday"
23
+ spec.add_dependency "faraday_middleware"
24
+ spec.add_dependency "hashie"
25
+
26
+ spec.add_development_dependency "bundler", "~> 1.8"
27
+ spec.add_development_dependency "rake", "~> 10.0"
28
+ spec.add_development_dependency "webmock"
29
+ end
@@ -0,0 +1,13 @@
1
+ class ShortcutHandler < YARD::Handlers::Ruby::Base
2
+ handles method_call(:shortcut)
3
+ namespace_only
4
+
5
+ def process
6
+ unless namespace.docstring.index('== Payload Shortcuts')
7
+ namespace.docstring += "\n\n== Payload Shortcuts\n"
8
+ end
9
+ shortcut = statement.parameters.first.jump(:ident).source
10
+ path = statement.parameters[1].jump(:string_content).source
11
+ namespace.docstring += "\n[+:#{shortcut}+] +#{path}+"
12
+ end
13
+ end
metadata ADDED
@@ -0,0 +1,152 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: paypoint-blue
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Laszlo Bacsi
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2015-03-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday_middleware
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: hashie
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.8'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.8'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '10.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '10.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: webmock
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: API client for PayPoint's 3rd generation PSP product a.k.a PayPoint Blue
98
+ email:
99
+ - lackac@lackac.hu
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - ".gitignore"
105
+ - ".travis.yml"
106
+ - ".yardopts"
107
+ - Gemfile
108
+ - LICENSE
109
+ - README.md
110
+ - Rakefile
111
+ - bin/console
112
+ - bin/setup
113
+ - lib/paypoint/blue.rb
114
+ - lib/paypoint/blue/api.rb
115
+ - lib/paypoint/blue/base.rb
116
+ - lib/paypoint/blue/body_extractor.rb
117
+ - lib/paypoint/blue/error.rb
118
+ - lib/paypoint/blue/faraday_runscope.rb
119
+ - lib/paypoint/blue/hash_key_converter.rb
120
+ - lib/paypoint/blue/hosted.rb
121
+ - lib/paypoint/blue/payload_builder.rb
122
+ - lib/paypoint/blue/raise_errors.rb
123
+ - lib/paypoint/blue/utils.rb
124
+ - lib/paypoint/blue/version.rb
125
+ - paypoint-blue.gemspec
126
+ - support/yard_ext.rb
127
+ homepage: https://github.com/CPlus/paypoint-blue
128
+ licenses:
129
+ - MIT
130
+ metadata: {}
131
+ post_install_message:
132
+ rdoc_options: []
133
+ require_paths:
134
+ - lib
135
+ required_ruby_version: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: '0'
140
+ required_rubygems_version: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ requirements: []
146
+ rubyforge_project:
147
+ rubygems_version: 2.4.5
148
+ signing_key:
149
+ specification_version: 4
150
+ summary: API client for PayPoint Blue
151
+ test_files: []
152
+ has_rdoc: