paypoint-blue 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.
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: