spook_and_pay 0.2.0.alpha
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 +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: []
|