spook_and_pay 0.2.0.alpha
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +52 -0
- data/lib/spook_and_pay.rb +19 -0
- data/lib/spook_and_pay/adapters.rb +1 -0
- data/lib/spook_and_pay/adapters/braintree.rb +109 -0
- data/lib/spook_and_pay/credit_card.rb +96 -0
- data/lib/spook_and_pay/erroring_reader.rb +23 -0
- data/lib/spook_and_pay/missing_value_error.rb +25 -0
- data/lib/spook_and_pay/providers.rb +2 -0
- data/lib/spook_and_pay/providers/base.rb +203 -0
- data/lib/spook_and_pay/providers/braintree.rb +277 -0
- data/lib/spook_and_pay/result.rb +68 -0
- data/lib/spook_and_pay/submission_error.rb +66 -0
- data/lib/spook_and_pay/transaction.rb +149 -0
- metadata +129 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 91f5461e5cc5382b312c2e0d9c7a1ddf576a14e2
|
4
|
+
data.tar.gz: 78f8ef0364586a4f8d22283b8ccbf79925068d66
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6061677abfdec2995ad03ef6de15d48308f2685f2cd91cbfff58f4a7fc19ef04a2a1adbf626efa3df1af88d951855661af4f49a24d634d21102965376ea479ea
|
7
|
+
data.tar.gz: 4b34cc7037809402145583ee019751230ebd3b4bbe04903d9794486c92530236a80a9d2e95c2318c44cdbaad565dd4b5b7e7da1ff56633c2ae8cc2e22d961fb9
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
Copyright © 2013 Spook and Puff
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
5
|
+
this software and associated documentation files (the “Software”), to deal in
|
6
|
+
the Software without restriction, including without limitation the rights to
|
7
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
8
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
9
|
+
so, subject to the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
12
|
+
copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
20
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
# Spook and Pay
|
2
|
+
|
3
|
+
A small library which wraps payment and credit card vaulting providers that support transparent redirect.
|
4
|
+
|
5
|
+
The aim is to make switching between providers easy or even support multiple providers within the same application.
|
6
|
+
|
7
|
+
Initially this library will support Braintree and SpreedlyCore, with more added as needed.
|
8
|
+
|
9
|
+
## Alpha Warning
|
10
|
+
|
11
|
+
This library is currently in-flight; you're welcome to hack on it, but it's unlikely to be usable.
|
12
|
+
|
13
|
+
## Usage
|
14
|
+
|
15
|
+
All actions are run via an instance of a `SpookAndPay::Providers::Base` subclass. Configuration is dependent on the particular provider you are using. For example, here is how you would configure Braintree:
|
16
|
+
|
17
|
+
```
|
18
|
+
provider = SpookAndPay::Providers::Braintree.new(
|
19
|
+
:development,
|
20
|
+
:merchant_id => "...",
|
21
|
+
:public_key => "...",
|
22
|
+
:secret_key => "...",
|
23
|
+
)
|
24
|
+
```
|
25
|
+
|
26
|
+
You can then use the provider instance to interrogate Braintree and to perform actions.
|
27
|
+
|
28
|
+
```
|
29
|
+
transaction = provider.transaction('...')
|
30
|
+
transaction.status # => :settling
|
31
|
+
transaction.can_refund? # => false
|
32
|
+
transaction.can_void? # => true
|
33
|
+
|
34
|
+
result = transaction.void!
|
35
|
+
result.successful? # => true
|
36
|
+
```
|
37
|
+
|
38
|
+
### Workflow
|
39
|
+
|
40
|
+
Currently SpookAndPay does not support the direct submission of payment details — credit card numbers etc — but instead relies on payment providers which feature transparent redirect/post for submission of details.
|
41
|
+
|
42
|
+
Direct submission of details will _never_ be supported, since it raises the specter of PCI-compliance.
|
43
|
+
|
44
|
+
### Transparent Redirect
|
45
|
+
|
46
|
+
TBD
|
47
|
+
|
48
|
+
## Todo
|
49
|
+
|
50
|
+
* Normalise transaction statuses across providers
|
51
|
+
* Normalise errors across providers
|
52
|
+
* Implement actions on CreditCard e.g. update, delete
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module SpookAndPay
|
2
|
+
|
3
|
+
end
|
4
|
+
|
5
|
+
require 'cgi'
|
6
|
+
require 'digest'
|
7
|
+
require 'openssl'
|
8
|
+
require 'net/http'
|
9
|
+
|
10
|
+
require 'braintree'
|
11
|
+
|
12
|
+
require 'spook_and_pay/submission_error'
|
13
|
+
require 'spook_and_pay/missing_value_error'
|
14
|
+
require 'spook_and_pay/erroring_reader'
|
15
|
+
require 'spook_and_pay/credit_card'
|
16
|
+
require 'spook_and_pay/result'
|
17
|
+
require 'spook_and_pay/transaction'
|
18
|
+
require 'spook_and_pay/adapters'
|
19
|
+
require 'spook_and_pay/providers'
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'spook_and_pay/adapters/braintree'
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module SpookAndPay
|
2
|
+
module Adapters
|
3
|
+
# A class which wraps the existing Braintree client and lets us use it in
|
4
|
+
# a sane way. Specifically, it lets us have multiple sets of credentials,
|
5
|
+
# whereas the default behaviour in the lib is to have them global
|
6
|
+
class Braintree
|
7
|
+
# Accessor for the Braintree::Gateway instance. In general should not be
|
8
|
+
# accessed externally, but is put here for debugging etc.
|
9
|
+
attr_reader :gateway
|
10
|
+
|
11
|
+
# Constructs an instance of the Braintree gateway which it then acts as
|
12
|
+
# a proxy to.
|
13
|
+
#
|
14
|
+
# @param [:development, :test, :production] environment
|
15
|
+
# @param String merchant_id
|
16
|
+
# @param String public_key
|
17
|
+
# @param String private_key
|
18
|
+
def initialize(environment, merchant_id, public_key, private_key)
|
19
|
+
_environment = case environment
|
20
|
+
when :production then :production
|
21
|
+
when :development, :test then :sandbox
|
22
|
+
end
|
23
|
+
|
24
|
+
config = ::Braintree::Configuration.new(
|
25
|
+
:custom_user_agent => ::Braintree::Configuration.instance_variable_get(:@custom_user_agent),
|
26
|
+
:endpoint => ::Braintree::Configuration.instance_variable_get(:@endpoint),
|
27
|
+
:environment => _environment,
|
28
|
+
:logger => ::Braintree::Configuration.logger,
|
29
|
+
:merchant_id => merchant_id,
|
30
|
+
:private_key => private_key,
|
31
|
+
:public_key => public_key
|
32
|
+
)
|
33
|
+
|
34
|
+
@gateway = ::Braintree::Gateway.new(config)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Looks up the transaction from Braintree.
|
38
|
+
#
|
39
|
+
# @param String id
|
40
|
+
# @return [nil, Braintree::Transaction]
|
41
|
+
def transaction(id)
|
42
|
+
gateway.transaction.find(id)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Looks up credit card details from Braintree. It squashes NotFoundError
|
46
|
+
# and just returns nil instead.
|
47
|
+
#
|
48
|
+
# @param String id
|
49
|
+
# @return [Braintree::CreditCard, nil]
|
50
|
+
def credit_card(id)
|
51
|
+
begin
|
52
|
+
gateway.credit_card.find(id)
|
53
|
+
rescue ::Braintree::NotFoundError => e
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Generates the hash and query string that needs to be embedded inside
|
59
|
+
# of a form in order to interact with Braintree's transparent redirect.
|
60
|
+
#
|
61
|
+
# @param Hash data
|
62
|
+
#
|
63
|
+
# @return String
|
64
|
+
def transaction_data(data)
|
65
|
+
gateway.transparent_redirect.transaction_data(data)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Used to confirm the submission of purchase or authorize transactions
|
69
|
+
# via transparent redirect.
|
70
|
+
#
|
71
|
+
# @param String query_string
|
72
|
+
# @return [Braintree::SuccessfulResult, Braintree::ErrorResult]
|
73
|
+
def confirm(query_string)
|
74
|
+
gateway.transparent_redirect.confirm(query_string)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Captures the funds in an authorized transaction.
|
78
|
+
#
|
79
|
+
# @param String id
|
80
|
+
# @return [Braintree::SuccessfulResult, Braintree::ErrorResult]
|
81
|
+
def capture(id)
|
82
|
+
gateway.transaction.submit_for_settlement(id)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Refunds the funds in a settled transaction.
|
86
|
+
#
|
87
|
+
# @param String id
|
88
|
+
# @return [Braintree::SuccessfulResult, Braintree::ErrorResult]
|
89
|
+
def refund(id)
|
90
|
+
gateway.transaction.refund(id)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Voids a transaction.
|
94
|
+
#
|
95
|
+
# @param String id
|
96
|
+
# @return [Braintree::SuccessfulResult, Braintree::ErrorResult]
|
97
|
+
def void(id)
|
98
|
+
gateway.transaction.void(id)
|
99
|
+
end
|
100
|
+
|
101
|
+
# The target URL for transparent redirects.
|
102
|
+
#
|
103
|
+
# @return String
|
104
|
+
def transparent_redirect_url
|
105
|
+
gateway.transparent_redirect.url
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module SpookAndPay
|
2
|
+
# A simple, generic class which wraps the card details retrieved from a
|
3
|
+
# provider. This class is entirely read only, since it is only used to
|
4
|
+
# as part of inspecting a payment or handling errors.
|
5
|
+
class CreditCard
|
6
|
+
# This module adds the ::attr_erroring_reader to this class
|
7
|
+
extend SpookAndPay::ErroringReader
|
8
|
+
|
9
|
+
# The basic attributes of the credit card.
|
10
|
+
attr_reader :provider, :id
|
11
|
+
|
12
|
+
# The fields required for a credit card
|
13
|
+
FIELDS = [:number, :expiration_month, :expiration_year, :cvv, :card_type, :name, :valid, :expired].freeze
|
14
|
+
|
15
|
+
# Define readers for all the fields
|
16
|
+
attr_reader *FIELDS
|
17
|
+
|
18
|
+
# Define a subset of the readers as erroring
|
19
|
+
attr_erroring_reader :valid, :expired
|
20
|
+
|
21
|
+
# Construct a new credit card using the ID from the provider and a hash
|
22
|
+
# containing the values of the card.
|
23
|
+
#
|
24
|
+
# @param SpookAndPay::Providers::Base provider
|
25
|
+
# @param [Numeric, String] id
|
26
|
+
# @param Hash vals
|
27
|
+
# @option vals String :number
|
28
|
+
# @option vals [String, Numeric] :expiration_month
|
29
|
+
# @option vals [String, Numeric] :expiration_year
|
30
|
+
# @option vals [String, Numeric] :cvv
|
31
|
+
# @option vals String :card_type
|
32
|
+
# @option vals String :name
|
33
|
+
# @option vals [true, false] :expired
|
34
|
+
# @option vals [true, false] :valid
|
35
|
+
def initialize(provider, id, vals)
|
36
|
+
@provider = provider
|
37
|
+
@id = id
|
38
|
+
FIELDS.each {|f| instance_variable_set(:"@#{f}", vals[f]) if vals.has_key?(f)}
|
39
|
+
end
|
40
|
+
|
41
|
+
# A getter which takes the card number stored and generates a nice masked
|
42
|
+
# version. It also handles the case where the number isn't available and
|
43
|
+
# just returns nil instead.
|
44
|
+
#
|
45
|
+
# @return String
|
46
|
+
def number
|
47
|
+
if @number.nil? or @number.empty?
|
48
|
+
nil
|
49
|
+
else
|
50
|
+
case card_type
|
51
|
+
when 'american_express' then "XXXX-XXXXXX-#{@number}"
|
52
|
+
else "XXXX-XXXX-XXXX-#{@number}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Authorizes a payment of the specified amount. This generates a new
|
58
|
+
# transaction that must be later settled.
|
59
|
+
#
|
60
|
+
# @param [String, Numeric] amount
|
61
|
+
# @return SpookAndPay::Result
|
62
|
+
def authorize!(amount)
|
63
|
+
provider.authorize_via_credit_card(self, amount)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Generates a payment of the specified amount.
|
67
|
+
#
|
68
|
+
# @param [String, Numeric] amount
|
69
|
+
# @return SpookAndPay::Result
|
70
|
+
def purchase!(amount)
|
71
|
+
provider.purchase_via_credit_card(self, amount)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Deletes the credit card from the provider's vault.
|
75
|
+
#
|
76
|
+
# @return [true, false]
|
77
|
+
def delete!
|
78
|
+
provider.delete_credit_card(self)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Indicates if the card details are valid.
|
82
|
+
#
|
83
|
+
# @return [true, false]
|
84
|
+
def valid?
|
85
|
+
valid
|
86
|
+
end
|
87
|
+
|
88
|
+
# Indicates if the card is expired. This is not calculated, but instead
|
89
|
+
# determined by the provider.
|
90
|
+
#
|
91
|
+
# @return [true, false]
|
92
|
+
def expired?
|
93
|
+
expired
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module SpookAndPay
|
2
|
+
module ErroringReader
|
3
|
+
# Defines a set of readers which will error if the underlying ivar is nil.
|
4
|
+
# It is intended to be used with a sub-set of readers which are important,
|
5
|
+
# but which may be nil. This is preferable to returning nil, which is falsy
|
6
|
+
# and will screw up any predicates.
|
7
|
+
#
|
8
|
+
# @param Symbol ivars
|
9
|
+
# @return nil
|
10
|
+
def attr_erroring_reader(*ivars)
|
11
|
+
ivars.each do |i|
|
12
|
+
class_eval %{
|
13
|
+
def #{i}
|
14
|
+
raise MissingValueError.new(:#{i}, self) unless defined?(@#{i})
|
15
|
+
@#{i}
|
16
|
+
end
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
nil
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module SpookAndPay
|
2
|
+
# A simple error class used to capture situations where the user is
|
3
|
+
# attampting to access a value, but it is not available. This is
|
4
|
+
# unfortunately necessary due to the way some providers do or do not
|
5
|
+
# return certain fields. Rather than allow comparison with nil values
|
6
|
+
# we throw this error.
|
7
|
+
class MissingValueError < StandardError
|
8
|
+
# When instancing this error, it needs to have enough information to point
|
9
|
+
# the user to the source.
|
10
|
+
#
|
11
|
+
# @param [String, Symbol] field
|
12
|
+
# @param Class record
|
13
|
+
def initialize(field, record)
|
14
|
+
@field = field
|
15
|
+
@record = record
|
16
|
+
end
|
17
|
+
|
18
|
+
# Human readable error message.
|
19
|
+
#
|
20
|
+
# @return String
|
21
|
+
def to_s
|
22
|
+
"The field #{@field} is missing for #{@record.class}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,203 @@
|
|
1
|
+
module SpookAndPay
|
2
|
+
module Providers
|
3
|
+
# The abstract class from which other Provider classes should inherit. This
|
4
|
+
# class is intended to behave more as a template than anything else. It
|
5
|
+
# provides very little in the way of actual implementation.
|
6
|
+
#
|
7
|
+
# To implement a provider all of the public methods of this class —
|
8
|
+
# excluding #initialize — must be implemented.
|
9
|
+
#
|
10
|
+
# Some features may not be supported by a provider, in which case the
|
11
|
+
# `NotSupportedError` should be raised in lieu of a real implementation.
|
12
|
+
class Base
|
13
|
+
# A hash which maps between the fields for a credit card and the actual
|
14
|
+
# form field names required by the provider.
|
15
|
+
#
|
16
|
+
# It should be over-ridden per provider.
|
17
|
+
FORM_FIELD_NAMES = {}.freeze
|
18
|
+
|
19
|
+
# An error used when validating the contents of an options hash. Since
|
20
|
+
# many of the methods on the provider classes take additional arguments
|
21
|
+
# as a Hash, it's important to make sure we give good errors when they
|
22
|
+
# are missing.
|
23
|
+
class InvalidOptionsError < StandardError
|
24
|
+
def initialize(errors)
|
25
|
+
@errors = errors
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_s
|
29
|
+
"You have missed, or provided invalid options"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# An error for indicating actions that are not supported by a particular
|
34
|
+
# provider. In general, it will be the Base subclasses that define thier
|
35
|
+
# own versions of the the method that throw this error.
|
36
|
+
class NotSupportedError < StandardError
|
37
|
+
def to_s
|
38
|
+
"This action is not supported by this provider."
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Basic attributes
|
43
|
+
attr_reader :environment, :config
|
44
|
+
|
45
|
+
# @param [:production, :development, :test] env
|
46
|
+
# @param Hash config
|
47
|
+
#
|
48
|
+
# @return nil
|
49
|
+
def initialize(env, config)
|
50
|
+
@environment = env
|
51
|
+
@config = config
|
52
|
+
|
53
|
+
nil
|
54
|
+
end
|
55
|
+
|
56
|
+
# Retrieves the payment method details from the provider's vault.
|
57
|
+
#
|
58
|
+
# @param String id
|
59
|
+
#
|
60
|
+
# @return [SpookAndPay::CreditCard, nil]
|
61
|
+
def credit_card(id)
|
62
|
+
raise NotImplementedError
|
63
|
+
end
|
64
|
+
|
65
|
+
# Retrieves a credit card from the provider based on the transaction
|
66
|
+
# or transaction id provided. Some providers may not support this action.
|
67
|
+
#
|
68
|
+
# @param [String, SpookAndPay::Transaction] transaction_or_id
|
69
|
+
# @return [SpookAndPay::CreditCard, nil]
|
70
|
+
def credit_card_from_transaction(transaction_or_id)
|
71
|
+
raise NotSupportedError
|
72
|
+
end
|
73
|
+
|
74
|
+
# Retrieves the transaction details from the provider's vault.
|
75
|
+
#
|
76
|
+
# @param String id
|
77
|
+
#
|
78
|
+
# @return [SpookAndPay::Transaction, nil]
|
79
|
+
def transaction(id)
|
80
|
+
raise NotImplementedError
|
81
|
+
end
|
82
|
+
|
83
|
+
# Returns a hash containing the details necessary for making a
|
84
|
+
# submission. If you know what you're doing, you can use this directly,
|
85
|
+
# but otherwise you should be using the form helpers.
|
86
|
+
#
|
87
|
+
# The details generated by this method are for submitting card details to
|
88
|
+
# the provider for storage. Billing etc has to be handled via a separate
|
89
|
+
# step after submission.
|
90
|
+
#
|
91
|
+
# The arguments for this are specific to each provider implementation,
|
92
|
+
# but they all return a Hash with the same keys, like so:
|
93
|
+
# {
|
94
|
+
# :url => "...",
|
95
|
+
# :hidden_fields => {...},
|
96
|
+
# :field_names => {...}
|
97
|
+
# }
|
98
|
+
#
|
99
|
+
# Where :url is the target URL, :hidden_fields should be embedded in a
|
100
|
+
# form as they are and :field_names provide the mapping between known
|
101
|
+
# keys like :number and :cvv to the input names required by the provider.
|
102
|
+
def prepare_payment_submission(*args)
|
103
|
+
raise NotImplementedError
|
104
|
+
end
|
105
|
+
|
106
|
+
# Confirms the submission of payment details to the provider.
|
107
|
+
#
|
108
|
+
# The arguments for this method are specific to a provider.
|
109
|
+
def confirm_payment_submission(*args)
|
110
|
+
raise NotImplementedError
|
111
|
+
end
|
112
|
+
|
113
|
+
# Captures funds that have been pre-authorized.
|
114
|
+
#
|
115
|
+
# This should not be called directly. Instead, use the #capture! method
|
116
|
+
# provided by a Transaction instance.
|
117
|
+
#
|
118
|
+
# @param [SpookAndPay::Transaction, String] id
|
119
|
+
# @return SpookAndPay::Result
|
120
|
+
def capture_transaction(id)
|
121
|
+
raise NotImplementedError
|
122
|
+
end
|
123
|
+
|
124
|
+
# Refunds the amount of money captured in a transaction.
|
125
|
+
#
|
126
|
+
# This should not be called directly. Instead, use the #refund! method
|
127
|
+
# provided by a Transaction instance.
|
128
|
+
#
|
129
|
+
# @param [SpookAndPay::Transaction, String] id
|
130
|
+
# @return SpookAndPay::Result
|
131
|
+
def refund_transaction(id)
|
132
|
+
raise NotImplementedError
|
133
|
+
end
|
134
|
+
|
135
|
+
# Voids an authorization.
|
136
|
+
#
|
137
|
+
# This should not be called directly. Instead, use the #void! method
|
138
|
+
# provided by a Transaction instance.
|
139
|
+
#
|
140
|
+
# @param [SpookAndPay::Transaction, String] id
|
141
|
+
# @return SpookAndPay::Result
|
142
|
+
# @api private
|
143
|
+
# @abstract Subclass to implement
|
144
|
+
def void_transaction(id)
|
145
|
+
raise NotImplementedError
|
146
|
+
end
|
147
|
+
|
148
|
+
# Authorizes a payment against a credit card
|
149
|
+
#
|
150
|
+
# This should not be called directly. Instead, use the #authorize! method
|
151
|
+
# provided by a CreditCard instance.
|
152
|
+
#
|
153
|
+
# @param [SpookAndPay::CreditCard, String] id
|
154
|
+
# @param [String, Numeric] amount
|
155
|
+
# @return SpookAndPay::Result
|
156
|
+
# @api private
|
157
|
+
# @abstract Subclass to implement
|
158
|
+
def authorize_via_credit_card(id, amount)
|
159
|
+
raise NotImplementedError
|
160
|
+
end
|
161
|
+
|
162
|
+
# Creates a purchase against a credit card.
|
163
|
+
#
|
164
|
+
# This should not be called directly. Instead, use the #purchase! method
|
165
|
+
# provided by a CreditCard instance.
|
166
|
+
#
|
167
|
+
# @param [SpookAndPay::CreditCard, String] id
|
168
|
+
# @param [String, Numeric] amount
|
169
|
+
# @return SpookAndPay::Result
|
170
|
+
# @api private
|
171
|
+
# @abstract Subclass to implement
|
172
|
+
def purchase_via_credit_card(id, amount)
|
173
|
+
raise NotImplementedError
|
174
|
+
end
|
175
|
+
|
176
|
+
# Removes payment details from the provider's vault.
|
177
|
+
#
|
178
|
+
# This should not be called directly. Instead, use the #delete! method
|
179
|
+
# provided by a CreditCard instance.
|
180
|
+
#
|
181
|
+
# @param [SpookAndPay::CreditCard, String] id
|
182
|
+
# @return SpookAndPay::Result
|
183
|
+
# @api private
|
184
|
+
# @abstract Subclass to implement
|
185
|
+
def delete_credit_card(id)
|
186
|
+
raise NotImplementedError
|
187
|
+
end
|
188
|
+
|
189
|
+
private
|
190
|
+
|
191
|
+
# Extracts a transaction ID from it's target.
|
192
|
+
#
|
193
|
+
# @param [SpookAndPay::Transaction, String]
|
194
|
+
# @return String
|
195
|
+
def transaction_id(id)
|
196
|
+
case id
|
197
|
+
when SpookAndPay::Transaction then id.id
|
198
|
+
else id
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
@@ -0,0 +1,277 @@
|
|
1
|
+
module SpookAndPay
|
2
|
+
module Providers
|
3
|
+
class Braintree < Base
|
4
|
+
FORM_FIELD_NAMES = {
|
5
|
+
:name => "transaction[credit_card][cardholder_name]",
|
6
|
+
:number => "transaction[credit_card][number]",
|
7
|
+
:expiration_month => "transaction[credit_card][expiration_month]",
|
8
|
+
:expiration_year => "transaction[credit_card][expiration_year]",
|
9
|
+
:cvv => "transaction[credit_card][cvv]"
|
10
|
+
}.freeze
|
11
|
+
|
12
|
+
attr_reader :adapter
|
13
|
+
|
14
|
+
# @param Hash config
|
15
|
+
# @option config String :merchant_id
|
16
|
+
# @option config String :public_key
|
17
|
+
# @option config String :private_key
|
18
|
+
def initialize(env, config)
|
19
|
+
@adapter = SpookAndPay::Adapters::Braintree.new(
|
20
|
+
env,
|
21
|
+
config[:merchant_id],
|
22
|
+
config[:public_key],
|
23
|
+
config[:private_key]
|
24
|
+
)
|
25
|
+
|
26
|
+
super(env, config)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Braintree specific version of this method. Can be used to either
|
30
|
+
# authorize a payment — and capture it later — or submit a payment for
|
31
|
+
# settlement immediately. This is done via the type param.
|
32
|
+
#
|
33
|
+
# Because Braintree accepts payment details and processes payment in a
|
34
|
+
# single step, this method must also be provided with an amount.
|
35
|
+
#
|
36
|
+
# @param [:purchase, :authorize] type
|
37
|
+
# @param String redirect_url
|
38
|
+
# @param [String, Numeric] amount
|
39
|
+
# @param Hash opts
|
40
|
+
# @option opts [true, false] :vault
|
41
|
+
# @return Hash
|
42
|
+
def prepare_payment_submission(type, redirect_url, amount, opts = {})
|
43
|
+
payload = {
|
44
|
+
:transaction => {:type => 'sale', :amount => amount},
|
45
|
+
:redirect_url => redirect_url
|
46
|
+
}
|
47
|
+
|
48
|
+
if opts[:vault]
|
49
|
+
(payload[:transaction][:options] ||= {})[:store_in_vault] = true
|
50
|
+
end
|
51
|
+
|
52
|
+
if type == :purchase
|
53
|
+
(payload[:transaction][:options] ||= {})[:submit_for_settlement] = true
|
54
|
+
end
|
55
|
+
|
56
|
+
{
|
57
|
+
:url => adapter.transparent_redirect_url,
|
58
|
+
:hidden_fields => {:tr_data => adapter.transaction_data(payload)},
|
59
|
+
:field_names => self.class::FORM_FIELD_NAMES
|
60
|
+
}
|
61
|
+
end
|
62
|
+
|
63
|
+
# Confirms the submission of payment details to the provider.
|
64
|
+
#
|
65
|
+
# @param String query_string
|
66
|
+
#
|
67
|
+
# @return SpookAndPay::Result
|
68
|
+
def confirm_payment_submission(query_string)
|
69
|
+
result = adapter.confirm(query_string)
|
70
|
+
|
71
|
+
case result
|
72
|
+
when ::Braintree::SuccessfulResult
|
73
|
+
SpookAndPay::Result.new(
|
74
|
+
true,
|
75
|
+
result,
|
76
|
+
:credit_card => extract_credit_card(result.transaction.credit_card_details, true, false),
|
77
|
+
:transaction => extract_transaction(result.transaction)
|
78
|
+
)
|
79
|
+
when ::Braintree::ErrorResult
|
80
|
+
SpookAndPay::Result.new(
|
81
|
+
false,
|
82
|
+
result,
|
83
|
+
:credit_card => extract_credit_card(result.params[:transaction][:credit_card], false, false),
|
84
|
+
:transaction => extract_transaction(result.params[:transaction]),
|
85
|
+
:errors => extract_errors(result)
|
86
|
+
)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def credit_card(id)
|
91
|
+
result = adapter.credit_card(id)
|
92
|
+
extract_credit_card(result, true, false) if result
|
93
|
+
end
|
94
|
+
|
95
|
+
def credit_card_from_transaction(id)
|
96
|
+
_id = id.is_a?(String) ? id : id.id
|
97
|
+
result = adapter.transaction(_id)
|
98
|
+
extract_credit_card(result.credit_card_details, true, false)
|
99
|
+
end
|
100
|
+
|
101
|
+
def transaction(id)
|
102
|
+
result = adapter.transaction(id)
|
103
|
+
extract_transaction(result) if result
|
104
|
+
end
|
105
|
+
|
106
|
+
def capture_transaction(id)
|
107
|
+
result = adapter.capture(transaction_id(id))
|
108
|
+
generate_result(result)
|
109
|
+
end
|
110
|
+
|
111
|
+
def refund_transaction(id)
|
112
|
+
result = adapter.refund(transaction_id(id))
|
113
|
+
generate_result(result)
|
114
|
+
end
|
115
|
+
|
116
|
+
def void_transaction(id)
|
117
|
+
result = adapter.void(transaction_id(id))
|
118
|
+
generate_result(result)
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
# Maps the error codes returned by Braintree to a triple of target, type
|
124
|
+
# and field used by the SubmissionError class.
|
125
|
+
#
|
126
|
+
# The key is the error code from Braintree. The first entry in the triple
|
127
|
+
# is the specific portion of the transaction that has the error. The
|
128
|
+
# second is the type of error and the third is the field — if any — it
|
129
|
+
# applies to.
|
130
|
+
ERROR_CODE_MAPPING = {
|
131
|
+
"81715" => [:credit_card, :invalid_number, :number],
|
132
|
+
"81725" => [:credit_card, :number_required, :number],
|
133
|
+
"81703" => [:credit_card, :type_not_accepted, :card_type],
|
134
|
+
"81716" => [:credit_card, :wrong_length, :number],
|
135
|
+
"81712" => [:credit_card, :invalid_expiration_month, :expiration_month],
|
136
|
+
"81713" => [:credit_card, :invalid_expiration_year, :expiration_year],
|
137
|
+
"81707" => [:credit_card, :invalid_cvv, :cvv],
|
138
|
+
"91507" => [:transaction, :cannot_capture, :status],
|
139
|
+
"91506" => [:transaction, :cannot_refund, :status],
|
140
|
+
"91504" => [:transaction, :cannot_void, :status]
|
141
|
+
}.freeze
|
142
|
+
|
143
|
+
# Extracts errors from the collection returned by Brain tree and coerces
|
144
|
+
# them into an array of SubmissionError.
|
145
|
+
#
|
146
|
+
# @param Braintree:ErrorResult result
|
147
|
+
# @return Array<SpookAndPay::Providers::Base::SubmissionError>
|
148
|
+
def extract_errors(result)
|
149
|
+
result.errors.map do |e|
|
150
|
+
mapping = ERROR_CODE_MAPPING[e.code]
|
151
|
+
if mapping
|
152
|
+
SubmissionError.new(*mapping, e)
|
153
|
+
else
|
154
|
+
SubmissionError.new(:unknown, :unknown, :unknown, e)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# A generic method for generating results on actions. It doesn't capture
|
160
|
+
# anything action specific i.e. it might need to be replaced later.
|
161
|
+
#
|
162
|
+
# @param [Braintree::SuccessfulResult, Braintree:ErrorResult] result
|
163
|
+
# @return SpookAndPay::Result
|
164
|
+
def generate_result(result)
|
165
|
+
case result
|
166
|
+
when ::Braintree::SuccessfulResult
|
167
|
+
SpookAndPay::Result.new(
|
168
|
+
true,
|
169
|
+
result,
|
170
|
+
:credit_card => extract_credit_card(result.transaction.credit_card_details, true, false),
|
171
|
+
:transaction => extract_transaction(result.transaction)
|
172
|
+
)
|
173
|
+
when ::Braintree::ErrorResult
|
174
|
+
SpookAndPay::Result.new(
|
175
|
+
false,
|
176
|
+
result,
|
177
|
+
:errors => extract_errors(result)
|
178
|
+
)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# Extracts credit card details from a payload extracted from a result.
|
183
|
+
# It could be either a Hash, Braintree::CreditCard or
|
184
|
+
# Braintree::Transaction::CreditCardDetails. BOO!
|
185
|
+
#
|
186
|
+
# @param [Hash, Braintree::CreditCard, Braintree::Transaction::CreditCardDetails] card
|
187
|
+
# @param [true, false] valid
|
188
|
+
# @param [true, false] expired
|
189
|
+
# @return SpookAndPay::CreditCard
|
190
|
+
#
|
191
|
+
# @todo figure out validity and expiry ourselves
|
192
|
+
def extract_credit_card(card, valid, expired)
|
193
|
+
opts = case card
|
194
|
+
when Hash
|
195
|
+
{
|
196
|
+
:token => card[:token],
|
197
|
+
:card_type => card[:card_type],
|
198
|
+
:number => card[:last_4],
|
199
|
+
:name => card[:cardholder_name],
|
200
|
+
:expiration_month => card[:expiration_month],
|
201
|
+
:expiration_year => card[:expiration_year]
|
202
|
+
}
|
203
|
+
else
|
204
|
+
{
|
205
|
+
:token => card.token,
|
206
|
+
:card_type => card.card_type,
|
207
|
+
:number => card.last_4,
|
208
|
+
:name => card.cardholder_name,
|
209
|
+
:expiration_month => card.expiration_month,
|
210
|
+
:expiration_year => card.expiration_year
|
211
|
+
}
|
212
|
+
end
|
213
|
+
|
214
|
+
SpookAndPay::CreditCard.new(self, opts.delete(:token), opts)
|
215
|
+
end
|
216
|
+
|
217
|
+
# Extracts transaction details from whatever payload is passed in. This
|
218
|
+
# might be Hash or a Braintree:Transaction.
|
219
|
+
#
|
220
|
+
# @param [Hash, Braintree::Transaction] result
|
221
|
+
# @param [true, false] successful
|
222
|
+
# @param Hash payload
|
223
|
+
# @return SpookAndPay::Transaction
|
224
|
+
#
|
225
|
+
# @todo Coerce type into what we know is valid
|
226
|
+
def extract_transaction(result, payload = {})
|
227
|
+
case result
|
228
|
+
when Hash
|
229
|
+
SpookAndPay::Transaction.new(
|
230
|
+
self,
|
231
|
+
result[:id],
|
232
|
+
coerce_transaction_status(result[:status]),
|
233
|
+
result,
|
234
|
+
:type => result[:type].to_sym,
|
235
|
+
:created_at => result[:created_at],
|
236
|
+
:amount => result[:amount]
|
237
|
+
)
|
238
|
+
else
|
239
|
+
SpookAndPay::Transaction.new(
|
240
|
+
self,
|
241
|
+
result.id,
|
242
|
+
coerce_transaction_status(result.status),
|
243
|
+
result,
|
244
|
+
:type => result.type.to_sym,
|
245
|
+
:created_at => result.created_at,
|
246
|
+
:amount => result.amount
|
247
|
+
)
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
# Coerces the status into a value expected by the Transaction class.
|
252
|
+
#
|
253
|
+
# @param [String, nil] status
|
254
|
+
# @return [String, nil]
|
255
|
+
def coerce_transaction_status(status)
|
256
|
+
case status
|
257
|
+
when 'submitted_for_settlement' then 'settling'
|
258
|
+
else status
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
# Based on the status of a transaction, make some determination of how of
|
263
|
+
# whether or not is is successful.
|
264
|
+
#
|
265
|
+
# @param String status
|
266
|
+
# @return [true, false]
|
267
|
+
#
|
268
|
+
# @todo Expand this to be more robust.
|
269
|
+
def coerce_transaction_success(status)
|
270
|
+
case status
|
271
|
+
when 'authorized' then true
|
272
|
+
else false
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module SpookAndPay
|
2
|
+
# A small convenience class which wraps any results coming back from a
|
3
|
+
# provider. This is never instanced directly, but instead instances are
|
4
|
+
# created by the Provider classes.
|
5
|
+
class Result
|
6
|
+
# Readers for the various portions of a result's payload. Depending on the
|
7
|
+
# type of request any of these may be nil.
|
8
|
+
attr_reader :transaction, :credit_card, :raw, :errors
|
9
|
+
|
10
|
+
# @param [true, false] successful
|
11
|
+
# @param Class raw
|
12
|
+
# @param Hash opts
|
13
|
+
def initialize(successful, raw, opts = {})
|
14
|
+
@successful = successful
|
15
|
+
@raw = raw
|
16
|
+
@transaction = opts[:transaction] if opts.has_key?(:transaction)
|
17
|
+
@credit_card = opts[:credit_card] if opts.has_key?(:credit_card)
|
18
|
+
@errors = opts[:errors] || []
|
19
|
+
end
|
20
|
+
|
21
|
+
# Checks to see if a transaction is present.
|
22
|
+
#
|
23
|
+
# @return [true, false]
|
24
|
+
def transaction?
|
25
|
+
!transaction.nil?
|
26
|
+
end
|
27
|
+
|
28
|
+
# Checks to see if a credit card is present.
|
29
|
+
#
|
30
|
+
# @return [true, false]
|
31
|
+
def credit_card?
|
32
|
+
!credit_card.nil?
|
33
|
+
end
|
34
|
+
|
35
|
+
# Checks to see if any errors are present.
|
36
|
+
#
|
37
|
+
# @return [true, false]
|
38
|
+
def errors?
|
39
|
+
!errors.empty?
|
40
|
+
end
|
41
|
+
|
42
|
+
# Collects errors for a specific target, keyed by field.
|
43
|
+
#
|
44
|
+
# @param Symbol target
|
45
|
+
# @return Hash
|
46
|
+
def errors_for(target)
|
47
|
+
errors.select{|e| e.target == target}.reduce({}) do |h, e|
|
48
|
+
h[e.field] ||= []
|
49
|
+
h[e.field] << e
|
50
|
+
h
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# A nice alias for checking for success.
|
55
|
+
#
|
56
|
+
# @return [true, false]
|
57
|
+
def successful?
|
58
|
+
@successful
|
59
|
+
end
|
60
|
+
|
61
|
+
# A nice helper for checking for failure.
|
62
|
+
#
|
63
|
+
# @return [true, false]
|
64
|
+
def failure?
|
65
|
+
!@successful
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module SpookAndPay
|
2
|
+
# Class used to encapsulate the details of an error related to some
|
3
|
+
# interaction with the provider. It is generic in that it might apply to
|
4
|
+
# a specific part of the payload or it might capture more general details.
|
5
|
+
#
|
6
|
+
# It will also attempt translate errors into a human readable string. Where
|
7
|
+
# it cannot, it still exposes the raw results from the provider allowing
|
8
|
+
# debugging.
|
9
|
+
class SubmissionError
|
10
|
+
# A constant which defines the acceptable types of errors and which is
|
11
|
+
# also used to generate specific messages. Where the error is unknown,
|
12
|
+
# the consumer of this library will be directed to use the raw error
|
13
|
+
# generated by the underlying libraries.
|
14
|
+
ERROR_MESSAGES = {
|
15
|
+
:credit_card => {
|
16
|
+
:number_required => "number is required",
|
17
|
+
:invalid_number => "number is invalid",
|
18
|
+
:type_not_accepted => "card type is not accepted by this merchant",
|
19
|
+
:wrong_length => "number must be between 12 and 19 digits",
|
20
|
+
:invalid_expiration_month => "expiration month is invalid",
|
21
|
+
:invalid_expiration_year => "expiration year is invalid",
|
22
|
+
:invalid_cvv => "CVV must be three digits"
|
23
|
+
},
|
24
|
+
:transaction => {
|
25
|
+
:cannot_capture => "must be authorized in order to capture funds",
|
26
|
+
:cannot_refund => "must be settled in order to refund",
|
27
|
+
:cannot_void => "must be authorized or settled in order to void"
|
28
|
+
},
|
29
|
+
:unknown => {
|
30
|
+
:unknown => "please refer to the #raw attribute of this error"
|
31
|
+
}
|
32
|
+
}.freeze
|
33
|
+
|
34
|
+
# Basic attributes
|
35
|
+
attr_reader :error_type, :message, :target, :raw, :field
|
36
|
+
|
37
|
+
# Generates a new error. Based on the target and error type, it can
|
38
|
+
# generate the appropriate error messages or otherwise fall back.
|
39
|
+
#
|
40
|
+
# @param Symbol error_type
|
41
|
+
# @param Symbol target
|
42
|
+
# @param [Symbol, nil] field
|
43
|
+
# @param Class raw
|
44
|
+
def initialize(target, error_type, field, raw)
|
45
|
+
@error_type = error_type
|
46
|
+
@target = target
|
47
|
+
@field = field
|
48
|
+
@raw = raw
|
49
|
+
end
|
50
|
+
|
51
|
+
# Indicates if this is an error that we don't know anything about.
|
52
|
+
#
|
53
|
+
# @return [true, false]
|
54
|
+
def unknown?
|
55
|
+
@error_type == :unknown
|
56
|
+
end
|
57
|
+
|
58
|
+
# Generates a human readable error message based on the target and
|
59
|
+
# error type.
|
60
|
+
#
|
61
|
+
# @return String
|
62
|
+
def message
|
63
|
+
ERROR_MESSAGES[target][error_type]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
module SpookAndPay
|
2
|
+
# A simple class representing an interaction with a provider. Each
|
3
|
+
# interaction has an ID, a type and may be successful or not. It is
|
4
|
+
# read-only.
|
5
|
+
class Transaction
|
6
|
+
# Basic attributes
|
7
|
+
attr_reader :provider, :id, :status, :raw
|
8
|
+
|
9
|
+
# Extra set of fields which may or may not be present depending on the
|
10
|
+
# provider.
|
11
|
+
FIELDS = [:created_at, :updated_at, :amount, :credit_card, :type].freeze
|
12
|
+
attr_reader *FIELDS
|
13
|
+
|
14
|
+
# The basic types for transactions.
|
15
|
+
TYPES = [:purchase, :authorize, :capture, :credit, :void].freeze
|
16
|
+
|
17
|
+
# Acceptable set of statuses.
|
18
|
+
STATUSES = [:authorized, :settling, :settled, :voided, :refunded, :gateway_rejected].freeze
|
19
|
+
|
20
|
+
# An error thrown when attempting to perform an action that is not allowed
|
21
|
+
# given a transaction's status.
|
22
|
+
class InvalidActionError < StandardError
|
23
|
+
# @param String id
|
24
|
+
# @param Symbol action
|
25
|
+
# @param Symbol status
|
26
|
+
def initialize(id, action, status)
|
27
|
+
@id = id
|
28
|
+
@action = action
|
29
|
+
@status = status
|
30
|
+
end
|
31
|
+
|
32
|
+
# Human readable message.
|
33
|
+
#
|
34
|
+
# @return String
|
35
|
+
def to_s
|
36
|
+
"Cannot perform the action '#{@action}' for transaction '#{@id}' while in status '#{@status}'"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# As a bare minimum the transaction captures the transaction ID, it's
|
41
|
+
# status and the raw response from the provider. Optionally, it can receive
|
42
|
+
# other fields via the opts hash.
|
43
|
+
#
|
44
|
+
# @param SpookAndPay::Providers::Base provider
|
45
|
+
# @param String id
|
46
|
+
# @param [String, nil] status
|
47
|
+
# @param Class raw
|
48
|
+
# @param Hash opts
|
49
|
+
# @option opts SpookAndPay::CreditCard :credit_card
|
50
|
+
# @option opts Time :created_at
|
51
|
+
# @option opts Time :updated_at
|
52
|
+
# @option opts BigDecimal :amount
|
53
|
+
# @option opts String :type
|
54
|
+
def initialize(provider, id, status, raw, opts = {})
|
55
|
+
@provider = provider
|
56
|
+
@id = id
|
57
|
+
@status = status.to_sym if status
|
58
|
+
@raw = raw
|
59
|
+
|
60
|
+
FIELDS.each {|f| instance_variable_set(:"@#{f}", opts[f]) if opts.has_key?(f)}
|
61
|
+
end
|
62
|
+
|
63
|
+
# Implements value comparison i.e. if class and ID match, they are the
|
64
|
+
# same.
|
65
|
+
#
|
66
|
+
# @param Class other
|
67
|
+
# @return [true, false]
|
68
|
+
def ==(other)
|
69
|
+
other.is_a?(SpookAndPay::Transaction) and other.id == id
|
70
|
+
end
|
71
|
+
|
72
|
+
# A simple predicate to see if the payment has been settled.
|
73
|
+
#
|
74
|
+
# @return [true, false]
|
75
|
+
def settled?
|
76
|
+
status == :settled
|
77
|
+
end
|
78
|
+
|
79
|
+
# A simple predicate which indicates if the payment is in the process of
|
80
|
+
# being settled.
|
81
|
+
#
|
82
|
+
# @return [true, false]
|
83
|
+
def settling?
|
84
|
+
status == :settling
|
85
|
+
end
|
86
|
+
|
87
|
+
# A simple predicate to check if the payment has been authorized.
|
88
|
+
#
|
89
|
+
# @return [true, false]
|
90
|
+
def authorized?
|
91
|
+
status == :authorized
|
92
|
+
end
|
93
|
+
|
94
|
+
# A predicate for checking if a transaction can be refunded. Only true if
|
95
|
+
# the status is :settled
|
96
|
+
#
|
97
|
+
# @return [true, false]
|
98
|
+
def can_refund?
|
99
|
+
status == :settled
|
100
|
+
end
|
101
|
+
|
102
|
+
# A predicate for checking if a transaction can be captured. Only true if
|
103
|
+
# the status is :authorized
|
104
|
+
#
|
105
|
+
# @return [true, false]
|
106
|
+
def can_capture?
|
107
|
+
status == :authorized
|
108
|
+
end
|
109
|
+
|
110
|
+
# A predicate for checking if a transaction can be voided. Only true if
|
111
|
+
# the status is :authorized or :submitted_for_settlement
|
112
|
+
#
|
113
|
+
# @return [true, false]
|
114
|
+
def can_void?
|
115
|
+
status == :authorized or status == :settling
|
116
|
+
end
|
117
|
+
|
118
|
+
# Refunds the transaction. The related credit card will be credited for
|
119
|
+
# the amount captured. It will only succeed for purchases or captured
|
120
|
+
# authorizations.
|
121
|
+
#
|
122
|
+
# @return SpookAndPay::Result
|
123
|
+
# @raises InvalidActionError
|
124
|
+
def refund!
|
125
|
+
raise InvalidActionError.new(id, :refund, status) unless can_refund?
|
126
|
+
provider.refund_transaction(self)
|
127
|
+
end
|
128
|
+
|
129
|
+
# Captures an authorized transaction. Will only capture the amount
|
130
|
+
# authorized and will fail if the transaction is already captured.
|
131
|
+
#
|
132
|
+
# @return SpookAndPay::Result
|
133
|
+
# @raises InvalidActionError
|
134
|
+
def capture!
|
135
|
+
raise InvalidActionError.new(id, :capture, status) unless can_capture?
|
136
|
+
provider.capture_transaction(self)
|
137
|
+
end
|
138
|
+
|
139
|
+
# Voids a transaction. Can only be done when the transaction is in the
|
140
|
+
# authorized status. Otherwise it must be refunded.
|
141
|
+
#
|
142
|
+
# @return SpookAndPay::Result
|
143
|
+
# @raises InvalidActionError
|
144
|
+
def void!
|
145
|
+
raise InvalidActionError.new(id, :void, status) unless can_void?
|
146
|
+
provider.void_transaction(self)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
metadata
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: spook_and_pay
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0.alpha
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Luke Sutton
|
8
|
+
- Ben Hull
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-08-26 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: braintree
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - '='
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: 2.25.0
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - '='
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: 2.25.0
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: rspec
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - '='
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: 2.14.1
|
35
|
+
type: :development
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - '='
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: 2.14.1
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: httparty
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - '='
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: 0.11.0
|
49
|
+
type: :development
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - '='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: 0.11.0
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: rack
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - '='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: 1.5.2
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - '='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 1.5.2
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: debugger
|
72
|
+
requirement: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - '='
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: 1.6.1
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - '='
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: 1.6.1
|
84
|
+
description:
|
85
|
+
email:
|
86
|
+
- lukeandben@spookandpuff.com
|
87
|
+
executables: []
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- lib/spook_and_pay/adapters/braintree.rb
|
92
|
+
- lib/spook_and_pay/adapters.rb
|
93
|
+
- lib/spook_and_pay/credit_card.rb
|
94
|
+
- lib/spook_and_pay/erroring_reader.rb
|
95
|
+
- lib/spook_and_pay/missing_value_error.rb
|
96
|
+
- lib/spook_and_pay/providers/base.rb
|
97
|
+
- lib/spook_and_pay/providers/braintree.rb
|
98
|
+
- lib/spook_and_pay/providers.rb
|
99
|
+
- lib/spook_and_pay/result.rb
|
100
|
+
- lib/spook_and_pay/submission_error.rb
|
101
|
+
- lib/spook_and_pay/transaction.rb
|
102
|
+
- lib/spook_and_pay.rb
|
103
|
+
- README.md
|
104
|
+
- LICENSE
|
105
|
+
homepage: http://spookandpuff.com
|
106
|
+
licenses: []
|
107
|
+
metadata: {}
|
108
|
+
post_install_message:
|
109
|
+
rdoc_options: []
|
110
|
+
require_paths:
|
111
|
+
- lib
|
112
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - '>='
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0'
|
117
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
118
|
+
requirements:
|
119
|
+
- - '>'
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: 1.3.1
|
122
|
+
requirements: []
|
123
|
+
rubyforge_project:
|
124
|
+
rubygems_version: 2.0.3
|
125
|
+
signing_key:
|
126
|
+
specification_version: 4
|
127
|
+
summary: A library for handling online payments using services providing transparent
|
128
|
+
redirects.
|
129
|
+
test_files: []
|