rack-payment 0.0.1
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.
- data/lib/rack-payment.rb +11 -0
- data/lib/rack-payment/billing_address.rb +30 -0
- data/lib/rack-payment/credit_card.rb +73 -0
- data/lib/rack-payment/helper.rb +221 -0
- data/lib/rack-payment/methods.rb +52 -0
- data/lib/rack-payment/payment.rb +252 -0
- data/lib/rack-payment/request.rb +228 -0
- data/lib/rack-payment/response.rb +55 -0
- data/lib/rack-payment/test.rb +42 -0
- data/lib/rack/payment.rb +1 -0
- data/lib/rack/payment/test.rb +1 -0
- metadata +74 -0
data/lib/rack-payment.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
$LOAD_PATH.unshift File.dirname(__FILE__)
|
2
|
+
|
3
|
+
%w( active_merchant rack bigdecimal forwardable ostruct erb ).each {|lib| require lib }
|
4
|
+
|
5
|
+
require 'rack-payment/payment'
|
6
|
+
require 'rack-payment/request'
|
7
|
+
require 'rack-payment/response'
|
8
|
+
require 'rack-payment/credit_card'
|
9
|
+
require 'rack-payment/billing_address'
|
10
|
+
require 'rack-payment/helper'
|
11
|
+
require 'rack-payment/methods'
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Rack #:nodoc:
|
2
|
+
class Payment #:nodoc:
|
3
|
+
|
4
|
+
class BillingAddress
|
5
|
+
attr_accessor :name, :address1, :city, :state, :zip, :country
|
6
|
+
|
7
|
+
def [] key
|
8
|
+
send key
|
9
|
+
end
|
10
|
+
|
11
|
+
def update options
|
12
|
+
options.each {|key, value| send "#{key}=", value }
|
13
|
+
end
|
14
|
+
|
15
|
+
def partially_filled_out?
|
16
|
+
%w( name address1 city state zip country ).each do |field|
|
17
|
+
return true unless send(field).nil?
|
18
|
+
end
|
19
|
+
|
20
|
+
return false
|
21
|
+
end
|
22
|
+
|
23
|
+
# Aliases
|
24
|
+
|
25
|
+
def street() address1 end
|
26
|
+
def street=(value) self.address1=(value) end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Rack #:nodoc:
|
2
|
+
class Payment #:nodoc:
|
3
|
+
|
4
|
+
class CreditCard
|
5
|
+
|
6
|
+
REQUIRED = %w( first_name last_name type number month year verification_value )
|
7
|
+
|
8
|
+
attr_accessor :active_merchant_card
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@active_merchant_card ||= ActiveMerchant::Billing::CreditCard.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def [] key
|
15
|
+
send key
|
16
|
+
end
|
17
|
+
|
18
|
+
def method_missing name, *args, &block
|
19
|
+
if active_merchant_card.respond_to?(name)
|
20
|
+
active_merchant_card.send(name, *args, &block)
|
21
|
+
else
|
22
|
+
super
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def partially_filled_out?
|
27
|
+
%w( type number verification_value month year first_name last_name ).each do |field|
|
28
|
+
return true unless send(field).nil?
|
29
|
+
end
|
30
|
+
|
31
|
+
return false
|
32
|
+
end
|
33
|
+
|
34
|
+
def fully_filled_out?
|
35
|
+
# errors.empty?
|
36
|
+
raise "Not yet spec'd"
|
37
|
+
end
|
38
|
+
|
39
|
+
def update options
|
40
|
+
options.each {|key, value| send "#{key}=", value }
|
41
|
+
end
|
42
|
+
|
43
|
+
# Aliases
|
44
|
+
|
45
|
+
def cvv() verification_value end
|
46
|
+
def cvv=(value) self.verification_value=(value) end
|
47
|
+
|
48
|
+
def expiration_year() year end
|
49
|
+
def expiration_year=(value) self.year=(value) end
|
50
|
+
|
51
|
+
def expiration_month() month end
|
52
|
+
def expiration_month=(value) self.month=(value) end
|
53
|
+
|
54
|
+
def type
|
55
|
+
active_merchant_card.type
|
56
|
+
end
|
57
|
+
|
58
|
+
def full_name
|
59
|
+
[ first_name, last_name ].compact.join(' ')
|
60
|
+
end
|
61
|
+
|
62
|
+
def errors
|
63
|
+
REQUIRED.inject([]) do |errors, required_attribute_name|
|
64
|
+
value = send required_attribute_name
|
65
|
+
errors << "#{ required_attribute_name.titleize } is required" if value.nil? or value.empty?
|
66
|
+
errors
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,221 @@
|
|
1
|
+
module Rack #:nodoc:
|
2
|
+
class Payment #:nodoc:
|
3
|
+
|
4
|
+
# When you include {Rack::Payment::Methods} into your application, you
|
5
|
+
# get a {#payment} method/object which gives you an instance of {Rack::Payment::Helper}
|
6
|
+
#
|
7
|
+
# {Rack::Payment::Helper} is the main API for working with {Rack::Payment}. You use it to:
|
8
|
+
#
|
9
|
+
# * Set the {#amount} you want to charge someone
|
10
|
+
# * Spit out the HTML for a credit card / billing information {#form} into your own application
|
11
|
+
# * Set the {#credit_card} and {#billing_address} to be used when processing the payment
|
12
|
+
# * Get {#errors} if something didn't work
|
13
|
+
# * Get the {#response} from your billing gateway after charging (or attempting to charge) someone
|
14
|
+
# * Get the URL to the image for a {#paypal_express_button}
|
15
|
+
#
|
16
|
+
class Helper
|
17
|
+
extend Forwardable
|
18
|
+
|
19
|
+
def_delegators :response, :amount_paid, :success?,
|
20
|
+
:raw_authorize_response, :raw_authorize_response=,
|
21
|
+
:raw_capture_response, :raw_capture_response=,
|
22
|
+
:raw_express_response, :raw_express_response=
|
23
|
+
|
24
|
+
def_delegators :rack_payment, :gateway, :built_in_form_path, :logger, :logger=
|
25
|
+
|
26
|
+
attr_accessor :rack_payment, :amount, :credit_card, :billing_address, :errors, :use_express, :response
|
27
|
+
|
28
|
+
# @param [Rack::Payment]
|
29
|
+
def initialize rack_payment
|
30
|
+
@rack_payment = rack_payment
|
31
|
+
end
|
32
|
+
|
33
|
+
def cc
|
34
|
+
credit_card
|
35
|
+
end
|
36
|
+
|
37
|
+
def use_express
|
38
|
+
@use_express.nil? ? false : @use_express # default to false
|
39
|
+
end
|
40
|
+
|
41
|
+
def use_express?
|
42
|
+
self.use_express == true
|
43
|
+
end
|
44
|
+
|
45
|
+
def use_express!
|
46
|
+
self.use_express = true
|
47
|
+
end
|
48
|
+
|
49
|
+
# helper for getting the src of the express checkout image
|
50
|
+
def paypal_express_button
|
51
|
+
'https://www.paypal.com/en_US/i/btn/btn_xpressCheckout.gif'
|
52
|
+
end
|
53
|
+
|
54
|
+
def errors
|
55
|
+
@errors ||= []
|
56
|
+
end
|
57
|
+
|
58
|
+
def credit_card
|
59
|
+
@credit_card ||= CreditCard.new
|
60
|
+
end
|
61
|
+
|
62
|
+
def billing_address
|
63
|
+
@billing_address ||= BillingAddress.new
|
64
|
+
end
|
65
|
+
|
66
|
+
def response
|
67
|
+
@response ||= Response.new
|
68
|
+
end
|
69
|
+
|
70
|
+
def amount= value
|
71
|
+
@amount = BigDecimal(value.to_s)
|
72
|
+
end
|
73
|
+
|
74
|
+
def amount_in_cents
|
75
|
+
(amount * 100).to_i if amount
|
76
|
+
end
|
77
|
+
|
78
|
+
def card_or_address_partially_filled_out?
|
79
|
+
credit_card.partially_filled_out? or billing_address.partially_filled_out?
|
80
|
+
end
|
81
|
+
|
82
|
+
# The same as {#purchase} but it raises an exception on error.
|
83
|
+
def purchase! options
|
84
|
+
if response = purchase(options)
|
85
|
+
true
|
86
|
+
else
|
87
|
+
raise "Purchase failed. #{ errors.join(', ') }"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Move these out into a module or something?
|
92
|
+
|
93
|
+
def log_purchase_start transaction_id, options
|
94
|
+
logger.debug { "[#{transaction_id}] #purchase(#{options.inspect}) for amount_in_cents: #{ amount_in_cents.inspect }" } if logger
|
95
|
+
end
|
96
|
+
|
97
|
+
def log_invalid_credit_card transaction_id
|
98
|
+
logger.warn { "[#{transaction_id}] invalid credit card: #{ errors.inspect }" } if logger
|
99
|
+
end
|
100
|
+
|
101
|
+
def log_authorize_successful transaction_id, options
|
102
|
+
logger.debug { "[#{transaction_id}] #authorize(#{amount_in_cents.inspect}, <CreditCard for #{ credit_card.full_name.inspect }>, :ip => #{ options[:ip].inspect }) was successful" } if logger
|
103
|
+
end
|
104
|
+
|
105
|
+
def log_authorize_unsuccessful transaction_id, options
|
106
|
+
logger.debug { "[#{transaction_id}] #authorize(#{amount_in_cents.inspect}, <CreditCard for #{ credit_card.full_name.inspect }>, :ip => #{ options[:ip].inspect }) was unsuccessful: #{ errors.inspect }" } if logger
|
107
|
+
end
|
108
|
+
|
109
|
+
def log_capture_successful transaction_id
|
110
|
+
logger.debug { "[#{transaction_id}] #capture(#{amount_in_cents}, #{raw_authorize_response.authorization.inspect}) was successful" } if logger
|
111
|
+
end
|
112
|
+
|
113
|
+
def log_capture_unsuccessful transaction_id
|
114
|
+
logger.debug { "[#{transaction_id}] #capture(#{amount_in_cents}, #{raw_authorize_response.authorization.inspect}) was unsuccessful: #{ errors.inspect }" } if logger
|
115
|
+
end
|
116
|
+
|
117
|
+
# Fires off a purchase!
|
118
|
+
#
|
119
|
+
# This resets #errors and #response
|
120
|
+
#
|
121
|
+
def purchase options
|
122
|
+
transaction_id = DateTime.now.strftime('%Y-%m-%d %H:%M:%S %L') # %L to include milliseconds
|
123
|
+
log_purchase_start(transaction_id, options)
|
124
|
+
|
125
|
+
raise "#amount_in_cents must be greater than 0" unless amount_in_cents.to_i > 0
|
126
|
+
raise ArgumentError, "The :ip option is required when calling #purchase" unless options and options[:ip]
|
127
|
+
|
128
|
+
# Check for Credit Card errors
|
129
|
+
self.response = Response.new
|
130
|
+
self.errors = credit_card.errors # start off with any errors from the credit_card
|
131
|
+
|
132
|
+
# Try to #authorize (if no errors so far)
|
133
|
+
if errors.empty?
|
134
|
+
begin
|
135
|
+
# TODO should pass :billing_address, if the billing address isn't empty.
|
136
|
+
# fields: name, address1, city, state, country, zip.
|
137
|
+
# Some gateways (eg. PayPal Pro) require a billing_address!
|
138
|
+
self.raw_authorize_response = gateway.authorize amount_in_cents, credit_card.active_merchant_card, :ip => options[:ip]
|
139
|
+
unless raw_authorize_response.success?
|
140
|
+
errors << raw_authorize_response.message
|
141
|
+
log_authorize_unsuccessful(transaction_id, options)
|
142
|
+
end
|
143
|
+
rescue ActiveMerchant::Billing::Error => error
|
144
|
+
self.raw_authorize_response = OpenStruct.new :success? => false, :message => error.message, :authorization => nil
|
145
|
+
errors << error.message
|
146
|
+
log_authorize_unsuccessful(transaction_id, options)
|
147
|
+
end
|
148
|
+
else
|
149
|
+
log_invalid_credit_card(transaction_id)
|
150
|
+
end
|
151
|
+
|
152
|
+
# Try to #capture (if no errors so far)
|
153
|
+
if errors.empty?
|
154
|
+
log_authorize_successful(transaction_id, options)
|
155
|
+
begin
|
156
|
+
self.raw_capture_response = gateway.capture amount_in_cents, raw_authorize_response.authorization
|
157
|
+
unless raw_capture_response.success?
|
158
|
+
errors << raw_capture_response.message
|
159
|
+
log_capture_unsuccessful(transaction_id)
|
160
|
+
end
|
161
|
+
rescue ActiveMerchant::Billing::Error => error
|
162
|
+
self.raw_capture_response = OpenStruct.new :success? => false, :message => error.message
|
163
|
+
errors << raw_capture_response.message
|
164
|
+
log_capture_unsuccessful(transaction_id)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
log_capture_successful(transaction_id) if errors.empty?
|
169
|
+
|
170
|
+
return errors.empty?
|
171
|
+
end
|
172
|
+
|
173
|
+
# Returns the HTML for the built in form
|
174
|
+
#
|
175
|
+
# By default, the form will POST to the current URL (action='')
|
176
|
+
#
|
177
|
+
# You can pass a different URL for the form action
|
178
|
+
def form post_to = ''
|
179
|
+
css = ::File.dirname(__FILE__) + '/views/credit-card-and-billing-info-form.css'
|
180
|
+
view = ::File.dirname(__FILE__) + '/views/credit-card-and-billing-info-form.html.erb'
|
181
|
+
erb = ::File.read view
|
182
|
+
|
183
|
+
html = "<style type='text/css'>\n#{ ::File.read(css) }\n</style>"
|
184
|
+
html << ERB.new(erb).result(binding)
|
185
|
+
end
|
186
|
+
|
187
|
+
def options_for_expiration_month selected = nil
|
188
|
+
%w( 01 02 03 04 05 06 07 08 09 10 11 12 ).map { |month|
|
189
|
+
if selected and selected.to_s == month.to_s
|
190
|
+
"<option selected='selected'>#{ month }</option>"
|
191
|
+
else
|
192
|
+
"<option>#{ month }</option>"
|
193
|
+
end
|
194
|
+
}.join
|
195
|
+
end
|
196
|
+
|
197
|
+
def options_for_expiration_year selected = nil
|
198
|
+
(Date.today.year..(Date.today.year + 15)).map { |year|
|
199
|
+
if selected and selected.to_s == year.to_s
|
200
|
+
"<option selected='selected'>#{ year }</option>"
|
201
|
+
else
|
202
|
+
"<option>#{ year }</option>"
|
203
|
+
end
|
204
|
+
}.join
|
205
|
+
end
|
206
|
+
|
207
|
+
def options_for_credit_card_type selected = nil
|
208
|
+
[ ['visa', 'Visa'], ['master', 'MasterCard'], ['american_express', 'American Express'],
|
209
|
+
['discover', 'Discover'] ].map { |value, name|
|
210
|
+
|
211
|
+
if selected and selected.to_s == value.to_s
|
212
|
+
"<options value='#{ value }' selected='selected'>#{ name }</option>"
|
213
|
+
else
|
214
|
+
"<options value='#{ value }'>#{ name }</option>"
|
215
|
+
end
|
216
|
+
}.join
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
end
|
221
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Rack #:nodoc:
|
2
|
+
class Payment #:nodoc:
|
3
|
+
|
4
|
+
# This is intended to be included in your Rack/Sinatra/Rails application.
|
5
|
+
#
|
6
|
+
# It gives you a {#payment} object, which is the main API for working with {Rack::Payment}.
|
7
|
+
#
|
8
|
+
# It also gives you access to the instance of the {Rack::Payment} you included via {#rack_payment}
|
9
|
+
module Methods
|
10
|
+
|
11
|
+
# Returns an instance of {Rack::Payment::Helper}, which is the main API for working with {Rack::Payment}
|
12
|
+
#
|
13
|
+
# This assumes that this is available via env['rack.payment']
|
14
|
+
#
|
15
|
+
# If you override the {Rack::Payment#env_instance_variable}, you will need to
|
16
|
+
# pass that string as an option to {#rack_payment}
|
17
|
+
def payment env_instance_variable = Rack::Payment::DEFAULT_OPTIONS['env_instance_variable']
|
18
|
+
rack_payment_instance = rack_payment(env_instance_variable)
|
19
|
+
_request_env[ rack_payment_instance.env_helper_variable ] ||= Rack::Payment::Helper.new(rack_payment_instance)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns the instance of {Rack::Payment} your application is using.
|
23
|
+
#
|
24
|
+
# This assumes that this is available via env['rack.payment']
|
25
|
+
#
|
26
|
+
# If you override the {Rack::Payment#env_instance_variable}, you will need to
|
27
|
+
# pass that string as an option to {#rack_payment}
|
28
|
+
def rack_payment env_instance_variable = Rack::Payment::DEFAULT_OPTIONS['env_instance_variable']
|
29
|
+
_request_env[env_instance_variable]
|
30
|
+
end
|
31
|
+
|
32
|
+
# This method returns the Rack 'env' for the current request.
|
33
|
+
#
|
34
|
+
# This looks for #env or #request.env by default. If these don't return
|
35
|
+
# something, then we raise an exception and you should override this method
|
36
|
+
# so it returns the Rack env that we need.
|
37
|
+
#
|
38
|
+
# @private
|
39
|
+
def _request_env
|
40
|
+
if respond_to?(:env)
|
41
|
+
env
|
42
|
+
elsif respond_to?(:request) and request.respond_to?(:env)
|
43
|
+
request.env
|
44
|
+
else
|
45
|
+
raise "Couldn't find 'env' ... please override #_request_env"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,252 @@
|
|
1
|
+
module Rack #:nodoc:
|
2
|
+
|
3
|
+
# Rack::Payment is a Rack middleware for adding simple payment to your applications.
|
4
|
+
#
|
5
|
+
# use Rack::Payment, :gateway => 'paypal', :login => '...', :password => '...'
|
6
|
+
#
|
7
|
+
# Rack::Payment wraps {ActiveMerchant} so any gateway that {ActiveMerchant} supports
|
8
|
+
# *should* be usable in Rack::Payment.
|
9
|
+
#
|
10
|
+
# When you `#call` this middleware, a new {Rack::Payment::Request} instance
|
11
|
+
# gets created and it does the actual logic to figure out what to do.
|
12
|
+
#
|
13
|
+
class Payment
|
14
|
+
|
15
|
+
# Default file names that we used to look for yml configuration.
|
16
|
+
# You can change {Rack::Payment::yml_file_names} to override.
|
17
|
+
YML_FILE_NAMES = %w( .rack-payment.yml rack-payment.yml config/rack-payment.yml
|
18
|
+
../config/rack-payment payment.yml ../payment.yml config/payment.yml )
|
19
|
+
|
20
|
+
class << self
|
21
|
+
|
22
|
+
# A string of file names that we use to look for options,
|
23
|
+
# if options are not passes to the Rack::Payment constructor.
|
24
|
+
#
|
25
|
+
# @return [Array(String)]
|
26
|
+
attr_accessor :yml_file_names
|
27
|
+
|
28
|
+
# A standard logger. Defaults to nil. We assume that this has
|
29
|
+
# methods like #info that accept a String or a block.
|
30
|
+
#
|
31
|
+
# If this is set, new instances of Rack::Payment will use this logger by default.
|
32
|
+
#
|
33
|
+
# @return [Logger]
|
34
|
+
attr_accessor :logger
|
35
|
+
end
|
36
|
+
|
37
|
+
@yml_file_names = YML_FILE_NAMES
|
38
|
+
|
39
|
+
# These are the default values that we use to set the Rack::Payment attributes.
|
40
|
+
#
|
41
|
+
# These can all be overriden by passing the attribute name and new value to
|
42
|
+
# the Rack::Payment constructor:
|
43
|
+
#
|
44
|
+
# use Rack::Payment, :on_success => '/my-custom-page'
|
45
|
+
#
|
46
|
+
DEFAULT_OPTIONS = {
|
47
|
+
'on_success' => nil,
|
48
|
+
'built_in_form_path' => '/rack.payment/process',
|
49
|
+
'express_ok_path' => '/rack.payment/express.callback/ok',
|
50
|
+
'express_cancel_path' => '/rack.payment/express.callback/cancel',
|
51
|
+
'env_instance_variable' => 'rack.payment',
|
52
|
+
'env_helper_variable' => 'rack.payment.helper',
|
53
|
+
'session_variable' => 'rack.payment',
|
54
|
+
'rack_session_variable' => 'rack.session'
|
55
|
+
}
|
56
|
+
|
57
|
+
# The {Rack} application that this middleware was instantiated with
|
58
|
+
# @return [#call]
|
59
|
+
attr_accessor :app
|
60
|
+
|
61
|
+
# A standard logger. Defaults to nil. We assume that this has
|
62
|
+
# methods like #info that accept a String or a block
|
63
|
+
# @return [Logger]
|
64
|
+
attr_accessor :logger
|
65
|
+
|
66
|
+
def logger #:nodoc:
|
67
|
+
@logger ||= Rack::Payment.logger
|
68
|
+
end
|
69
|
+
|
70
|
+
# When a payment is successful, we redirect to this path, if set.
|
71
|
+
# If this is `nil`, we display our own confirmation page.
|
72
|
+
# @return [String, nil] (nil)
|
73
|
+
attr_accessor :on_success
|
74
|
+
|
75
|
+
# This is the path that the built-in form POSTs to when submitting
|
76
|
+
# Credit Card data. This is only used if you use the built_in_form.
|
77
|
+
# See {#use_built_in_form} to enable/disable using the default form
|
78
|
+
# @return [String]
|
79
|
+
attr_accessor :built_in_form_path
|
80
|
+
|
81
|
+
# TODO implement! NOT IMPLEMENTED YET
|
82
|
+
attr_accessor :use_built_in_form
|
83
|
+
|
84
|
+
# This is the path that we have express gateways (Paypal Express)
|
85
|
+
# redirect to, after a purchase has been made.
|
86
|
+
# @return [String]
|
87
|
+
attr_accessor :express_ok_path
|
88
|
+
|
89
|
+
# This is the path that we have express gateways (Paypal Express)
|
90
|
+
# redirect to if the user cancels their purchase.
|
91
|
+
# @return [String]
|
92
|
+
attr_accessor :express_cancel_path
|
93
|
+
|
94
|
+
# The name of the Rack env variable to use to access the instance
|
95
|
+
# of Rack::Payment that your application is using as middleware.
|
96
|
+
# @return [String]
|
97
|
+
attr_accessor :env_instance_variable
|
98
|
+
|
99
|
+
# The name of the Rack env variable to use to access data about
|
100
|
+
# the purchase being made. Getting this out of the Rack env
|
101
|
+
# gives you a {Rack::Payment::Helper} object.
|
102
|
+
# @return [String]
|
103
|
+
attr_accessor :env_helper_variable
|
104
|
+
|
105
|
+
# The name of the variable we put into the Rack::Session
|
106
|
+
# to store anything that {Rack::Payment} needs to keep track
|
107
|
+
# of between requests, eg. the amount that the user is trying
|
108
|
+
# to spend.
|
109
|
+
# @return [String]
|
110
|
+
attr_accessor :session_variable
|
111
|
+
|
112
|
+
# The name of the Rack env variable used for the Rack::Session,
|
113
|
+
# eg. `rack.session` (the default for Rack::Session::Cookie)
|
114
|
+
# @return [String]
|
115
|
+
attr_accessor :rack_session_variable
|
116
|
+
|
117
|
+
# The name of a type of ActiveMerchant::Billing::Gateway that we
|
118
|
+
# want to use, eg. 'paypal'. We use this to get the actual
|
119
|
+
# ActiveMerchant::Billing::Gateway class, eg. ActiveMerchant::Billing::Paypal
|
120
|
+
# @return [String]
|
121
|
+
attr_accessor :gateway_type
|
122
|
+
|
123
|
+
# The options that are passed to {Rack::Payment} when you include it as a
|
124
|
+
# middleware, minus the options that {Rack::Payment} uses.
|
125
|
+
#
|
126
|
+
# For example, if you instantiate a {Rack::Payment} middleware with Paypal,
|
127
|
+
# this will probably include :login, :password, and :signature
|
128
|
+
# @return [Hash]
|
129
|
+
attr_accessor :gateway_options
|
130
|
+
|
131
|
+
# Uses the #gateway_options to instantiate a [paypal] express gateway
|
132
|
+
#
|
133
|
+
# If your main gateway is a PaypayGateway, we'll make a PaypalExpressGateway
|
134
|
+
# If your main gateway is a BogusGateway, we'll make a BogusExpressGateway
|
135
|
+
#
|
136
|
+
# For any gateway, we'll try to make a *ExpressGateway
|
137
|
+
#
|
138
|
+
# This ONLY works for classes underneath ActiveMerchant::Billing
|
139
|
+
#
|
140
|
+
# @return [ActiveMerchant::Billing::Gateway]
|
141
|
+
attr_accessor :express_gateway
|
142
|
+
|
143
|
+
def express_gateway
|
144
|
+
@express_gateway ||= ActiveMerchant::Billing::Base.gateway(express_gateway_type).new(gateway_options)
|
145
|
+
end
|
146
|
+
|
147
|
+
# The name of the gateway to use for an express gateway.
|
148
|
+
#
|
149
|
+
# If our {#gateway} is a ActiveMerchant::Billing::PaypalGateway,
|
150
|
+
# this will return `paypal_express`
|
151
|
+
#
|
152
|
+
# Uses the class of #gateway to determine.
|
153
|
+
#
|
154
|
+
# @return [String]
|
155
|
+
def express_gateway_type
|
156
|
+
gateway.class.to_s.split('::').last.sub(/(\w+)Gateway$/, '\1_express')
|
157
|
+
end
|
158
|
+
|
159
|
+
# The actual instance of ActiveMerchant::Billing::Gateway object to use.
|
160
|
+
# Uses the #gateway_type and #gateway_options to instantiate a gateway.
|
161
|
+
# @return [ActiveMerchant::Billing::Gateway]
|
162
|
+
attr_accessor :gateway
|
163
|
+
|
164
|
+
def gateway
|
165
|
+
unless @gateway
|
166
|
+
begin
|
167
|
+
@gateway = ActiveMerchant::Billing::Base.gateway(gateway_type.to_s).new(gateway_options)
|
168
|
+
rescue NameError
|
169
|
+
# do nothing, @gateway should be nil because the gateway_type was invalid
|
170
|
+
end
|
171
|
+
end
|
172
|
+
@gateway
|
173
|
+
end
|
174
|
+
|
175
|
+
# @overload initialize(rack_application)
|
176
|
+
# Not yet implemented. This will search for a YML file or ENV variable to set options.
|
177
|
+
# @param [#call] The Rack application for this middleware
|
178
|
+
#
|
179
|
+
# @overload initialize(rack_application, options)
|
180
|
+
# Accepts a Hash of options where the :gateway option is used as the {#gateway_type}
|
181
|
+
# @param [#call] The Rack application for this middleware
|
182
|
+
# @param [Hash] Options for the gateway and for Rack::Payment
|
183
|
+
#
|
184
|
+
def initialize rack_application = nil, options = nil
|
185
|
+
options ||= {}
|
186
|
+
options = look_for_options_in_a_yml_file.merge(options) unless options['yml_config'] == false or options[:yml_config] == false
|
187
|
+
raise ArgumentError, "You must pass options (or put them in a yml file)." if options.empty?
|
188
|
+
|
189
|
+
@app = rack_application
|
190
|
+
@gateway_options = options # <---- need to remove *our* options from the gateway options!
|
191
|
+
@gateway_type = options['gateway'] || options[:gateway]
|
192
|
+
|
193
|
+
raise ArgumentError, 'You must pass a valid Rack application' unless @app.nil? or @app.respond_to?(:call)
|
194
|
+
raise ArgumentError, 'You must pass a valid Gateway' unless @gateway_type and gateway.is_a?(ActiveMerchant::Billing::Gateway)
|
195
|
+
|
196
|
+
DEFAULT_OPTIONS.each do |name, value|
|
197
|
+
# set the default
|
198
|
+
send "#{name}=", value
|
199
|
+
|
200
|
+
# override the value from options, if passed
|
201
|
+
if @gateway_options[name.to_s]
|
202
|
+
send "#{name.to_s}=", @gateway_options.delete(name.to_s)
|
203
|
+
elsif @gateway_options[name.to_s.to_sym]
|
204
|
+
send "#{name.to_s.to_sym}=", @gateway_options.delete(name.to_s.to_sym)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
# The main Rack #call method required by every Rack application / middleware.
|
210
|
+
# @param [Hash] The Rack Request environment variables
|
211
|
+
def call env
|
212
|
+
raise "Rack::Payment was not initialized with a Rack application and cannot be #call'd" unless @app
|
213
|
+
env[env_instance_variable] ||= self # make this instance available
|
214
|
+
return Request.new(env, self).finish # a Request object actually returns the response
|
215
|
+
end
|
216
|
+
|
217
|
+
# Looks for options in a yml file with a conventional name (using Rack::Payment.yml_file_names)
|
218
|
+
# Returns an empty Hash, if no options are found from a yml file.
|
219
|
+
# @return [Hash]
|
220
|
+
def look_for_options_in_a_yml_file
|
221
|
+
Rack::Payment.yml_file_names.each do |filename|
|
222
|
+
if ::File.file?(filename)
|
223
|
+
options = YAML.load_file(filename)
|
224
|
+
|
225
|
+
# if the YAML loaded something and it's a Hash
|
226
|
+
if options and options.is_a?(Hash)
|
227
|
+
|
228
|
+
# handle RACK_ENV / RAILS_ENV so you can put your test/development/etc in the same file
|
229
|
+
if ENV['RACK_ENV'] and options[ENV['RACK_ENV']].is_a?(Hash)
|
230
|
+
options = options[ENV['RACK_ENV']]
|
231
|
+
elsif ENV['RAILS_ENV'] and options[ENV['RAILS_ENV']].is_a?(Hash)
|
232
|
+
options = options[ENV['RAILS_ENV']]
|
233
|
+
end
|
234
|
+
|
235
|
+
return options
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
return {}
|
241
|
+
end
|
242
|
+
|
243
|
+
# Returns a new {Rack::Payment::Helper} instance which can be
|
244
|
+
# used to fire off single payments (without needing to make
|
245
|
+
# web requests).
|
246
|
+
# @return [Rack::Payment::Helper]
|
247
|
+
def payment
|
248
|
+
Rack::Payment::Helper.new(self)
|
249
|
+
end
|
250
|
+
|
251
|
+
end
|
252
|
+
end
|
@@ -0,0 +1,228 @@
|
|
1
|
+
module Rack #:nodoc:
|
2
|
+
class Payment #:nodoc:
|
3
|
+
|
4
|
+
# When you call {Rack::Payment#call}, a new {Rack::Payment::Request} instance
|
5
|
+
# gets created and it does the actual logic to figure out what to do.
|
6
|
+
#
|
7
|
+
# The {#finish} method "executes" this class (it figures out what to do).
|
8
|
+
#
|
9
|
+
class Request
|
10
|
+
extend Forwardable
|
11
|
+
|
12
|
+
def_delegators :payment_instance, :app, :gateway, :express_gateway, :on_success, :built_in_form_path,
|
13
|
+
:env_instance_variable, :env_helper_variable, :session_variable,
|
14
|
+
:rack_session_variable, :express_ok_path, :express_cancel_path,
|
15
|
+
:built_in_form_path
|
16
|
+
|
17
|
+
def_delegators :request, :params
|
18
|
+
|
19
|
+
def_delegators :payment, :errors, :credit_card, :billing_address
|
20
|
+
|
21
|
+
# TODO test these!!!
|
22
|
+
def express_ok_url
|
23
|
+
::File.join request.url.sub(request.path_info, ''), express_ok_path
|
24
|
+
end
|
25
|
+
def express_cancel_url
|
26
|
+
::File.join request.url.sub(request.path_info, ''), express_cancel_path
|
27
|
+
end
|
28
|
+
|
29
|
+
# @return [Hash] Raw Rack env Hash
|
30
|
+
attr_accessor :env
|
31
|
+
|
32
|
+
# An instance of Rack::Request that wraps our {#env}.
|
33
|
+
# It makes it much easier to access the params, path, method, etc.
|
34
|
+
# @return Rack::Request
|
35
|
+
attr_accessor :request
|
36
|
+
|
37
|
+
# Rack::Response that results from calling the actual Rack application.
|
38
|
+
# [Rack::Response]
|
39
|
+
attr_accessor :app_response
|
40
|
+
|
41
|
+
# Whether or not this request's POST came from our built in forms.
|
42
|
+
#
|
43
|
+
# * If true, we think the POST came from our form.
|
44
|
+
# * If false, we think the POST came from a user's custom form.
|
45
|
+
#
|
46
|
+
# @return [true, false]
|
47
|
+
attr_accessor :post_came_from_the_built_in_forms
|
48
|
+
|
49
|
+
# The instance of {Rack::Payment} that this Request is for
|
50
|
+
# @return [Rack::Payment]
|
51
|
+
attr_accessor :payment_instance
|
52
|
+
|
53
|
+
def post_came_from_the_built_in_forms?
|
54
|
+
post_came_from_the_built_in_forms == true
|
55
|
+
end
|
56
|
+
|
57
|
+
def payment
|
58
|
+
env[env_helper_variable] ||= Rack::Payment::Helper.new(payment_instance)
|
59
|
+
end
|
60
|
+
|
61
|
+
def session
|
62
|
+
env[rack_session_variable][session_variable] ||= {}
|
63
|
+
end
|
64
|
+
|
65
|
+
def amount_in_session
|
66
|
+
session[:amount]
|
67
|
+
end
|
68
|
+
|
69
|
+
def amount_in_session= value
|
70
|
+
session[:amount] = value
|
71
|
+
end
|
72
|
+
|
73
|
+
# Instantiates a {Rack::Payment::Request} object which basically wraps a
|
74
|
+
# single request and handles all of the logic to determine what to do.
|
75
|
+
#
|
76
|
+
# Calling {#finish} will return the actual Rack response
|
77
|
+
#
|
78
|
+
# @param [Hash] The Rack Request environment variables
|
79
|
+
# @param [Rack::Payment] The instance of Rack::Payment handling this request
|
80
|
+
def initialize env, payment_instance
|
81
|
+
@payment_instance = payment_instance
|
82
|
+
|
83
|
+
self.env = env
|
84
|
+
self.request = Rack::Request.new @env
|
85
|
+
|
86
|
+
raw_rack_response = app.call env
|
87
|
+
self.app_response = Rack::Response.new raw_rack_response[2], raw_rack_response[0], raw_rack_response[1]
|
88
|
+
end
|
89
|
+
|
90
|
+
# Generates and returns the final rack response.
|
91
|
+
#
|
92
|
+
# This "runs" the request. It's the main logic in {Rack::Payment}!
|
93
|
+
#
|
94
|
+
# @return [Array] A Rack response, eg. `[200, {}, ["Hello World"]]`
|
95
|
+
def finish
|
96
|
+
|
97
|
+
# The application returned a 402 ('Payment Required')
|
98
|
+
if app_response.status == 402
|
99
|
+
self.amount_in_session = payment.amount
|
100
|
+
|
101
|
+
return setup_express_purchase if payment.use_express?
|
102
|
+
|
103
|
+
if payment.card_or_address_partially_filled_out?
|
104
|
+
return process_credit_card
|
105
|
+
else
|
106
|
+
return credit_card_and_billing_info_response
|
107
|
+
end
|
108
|
+
|
109
|
+
# The requested path matches our built-in form
|
110
|
+
elsif request.path_info == built_in_form_path
|
111
|
+
self.post_came_from_the_built_in_forms = true
|
112
|
+
return process_credit_card
|
113
|
+
|
114
|
+
# The requested path matches our callback for express payments
|
115
|
+
elsif request.path_info == express_ok_path
|
116
|
+
return process_express_payment_callback
|
117
|
+
end
|
118
|
+
|
119
|
+
# If we haven't returned anything, there was no reason for the
|
120
|
+
# middleware to handle this request so we return the real
|
121
|
+
# application's response.
|
122
|
+
app_response.finish
|
123
|
+
end
|
124
|
+
|
125
|
+
# Gets parameters, attempts an #authorize call, attempts a #capture call,
|
126
|
+
# and renders the results.
|
127
|
+
def process_credit_card
|
128
|
+
payment.amount ||= amount_in_session
|
129
|
+
|
130
|
+
# The params *should* be set on the payment data object, but we accept
|
131
|
+
# POST requests too, so we check the POST variables for credit_card
|
132
|
+
# or billing_address fields
|
133
|
+
#
|
134
|
+
# TODO deprecate this in favor of the more conventional credit_card[number] syntax?
|
135
|
+
#
|
136
|
+
params.each do |field, value|
|
137
|
+
if field =~ /^credit_card_(\w+)/
|
138
|
+
payment.credit_card.update $1 => value
|
139
|
+
elsif field =~ /billing_address_(\w+)/
|
140
|
+
payment.billing_address.update $1 => value
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# We also accept credit_card[number] style params, which Rack supports
|
145
|
+
if params['credit_card'] and params['credit_card'].respond_to?(:each)
|
146
|
+
payment.credit_card.update params['credit_card']
|
147
|
+
end
|
148
|
+
if params['billing_address'] and params['billing_address'].respond_to?(:each)
|
149
|
+
payment.billing_address.update params['billing_address']
|
150
|
+
end
|
151
|
+
|
152
|
+
# Purchase!
|
153
|
+
if payment.purchase(:ip => request.ip)
|
154
|
+
render_on_success
|
155
|
+
else
|
156
|
+
render_on_error payment.errors
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def setup_express_purchase
|
161
|
+
# TODO we should get the callback URLs to use from the Rack::Purchase
|
162
|
+
# and they should be overridable
|
163
|
+
|
164
|
+
# TODO go BOOM if the express gateway isn't set!
|
165
|
+
|
166
|
+
# TODO catch exceptions
|
167
|
+
|
168
|
+
# TODO catch ! success?
|
169
|
+
response = express_gateway.setup_purchase payment.amount_in_cents, :ip => request.ip,
|
170
|
+
:return_url => express_ok_url,
|
171
|
+
:cancel_return_url => express_cancel_url
|
172
|
+
|
173
|
+
[ 302, {'Location' => express_gateway.redirect_url_for(response.token)}, ['Redirecting to PayPal Express Checkout'] ]
|
174
|
+
end
|
175
|
+
|
176
|
+
def render_on_error errors
|
177
|
+
if post_came_from_the_built_in_forms?
|
178
|
+
# we POSTed from our form, so let's re-render our form
|
179
|
+
credit_card_and_billing_info_response
|
180
|
+
else
|
181
|
+
# pass along the errors to the application's custom page, which should be the current URL
|
182
|
+
# so we can actually just re-call the same env (should display the form) using a GET
|
183
|
+
payment.errors = errors
|
184
|
+
new_env = env.clone
|
185
|
+
new_env['REQUEST_METHOD'] = 'GET'
|
186
|
+
new_env['PATH_INFO'] = on_success if request.path_info == express_ok_path # if express, we render on_success
|
187
|
+
app.call(new_env)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def render_on_success
|
192
|
+
if on_success
|
193
|
+
# on_success is overriden ... we #call the main application using the on_success path
|
194
|
+
new_env = env.clone
|
195
|
+
new_env['PATH_INFO'] = on_success
|
196
|
+
new_env['REQUEST_METHOD'] = 'GET'
|
197
|
+
app.call new_env
|
198
|
+
else
|
199
|
+
# on_success has not been overriden ... let's just display out own info
|
200
|
+
[ 200, {}, ["Order successful. You should have been charged #{ payment.amount }" ]]
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def credit_card_and_billing_info_response
|
205
|
+
form_html = payment.form built_in_form_path
|
206
|
+
layout = ::File.dirname(__FILE__) + '/views/layout.html'
|
207
|
+
html = ::File.read(layout)
|
208
|
+
html = html.sub 'CONTENT', form_html
|
209
|
+
|
210
|
+
[ 200, {'Content-Type' => 'text/html'}, html ]
|
211
|
+
end
|
212
|
+
|
213
|
+
def process_express_payment_callback
|
214
|
+
payment.amount ||= amount_in_session # gets lost because we're coming here directly from PayPal
|
215
|
+
|
216
|
+
details = express_gateway.details_for params['token']
|
217
|
+
|
218
|
+
payment.raw_express_response = express_gateway.purchase payment.amount_in_cents, :ip => request.ip,
|
219
|
+
:token => params['token'],
|
220
|
+
:payer_id => details.payer_id
|
221
|
+
|
222
|
+
render_on_success
|
223
|
+
end
|
224
|
+
|
225
|
+
end
|
226
|
+
|
227
|
+
end
|
228
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Rack #:nodoc:
|
2
|
+
class Payment #:nodoc:
|
3
|
+
|
4
|
+
# Represents the response you get when you try to make a purchase
|
5
|
+
# from ActiveMerchant
|
6
|
+
class Response
|
7
|
+
|
8
|
+
attr_accessor :raw_authorize_response
|
9
|
+
attr_accessor :raw_capture_response
|
10
|
+
attr_accessor :raw_express_response
|
11
|
+
|
12
|
+
alias auth raw_authorize_response
|
13
|
+
alias capture raw_capture_response
|
14
|
+
alias express raw_express_response
|
15
|
+
|
16
|
+
def has_responses?
|
17
|
+
auth or capture or express
|
18
|
+
end
|
19
|
+
|
20
|
+
def express?
|
21
|
+
express != nil
|
22
|
+
end
|
23
|
+
|
24
|
+
def amount_paid
|
25
|
+
if success?
|
26
|
+
if express?
|
27
|
+
express_amound_paid
|
28
|
+
else
|
29
|
+
(raw_capture_response.params['paid_amount'].to_f / 100)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def express_amound_paid
|
35
|
+
if success?
|
36
|
+
raw_express_response.params['gross_amount'].to_f
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def success?
|
41
|
+
if express?
|
42
|
+
express_success?
|
43
|
+
else
|
44
|
+
auth and auth.success? and capture and capture.success?
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def express_success?
|
49
|
+
raw_express_response.success?
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# Helpers for testing Rack::Payment
|
2
|
+
|
3
|
+
require 'ostruct'
|
4
|
+
|
5
|
+
module ActiveMerchant #:nodoc:
|
6
|
+
module Billing #:nodoc:
|
7
|
+
|
8
|
+
# ...
|
9
|
+
class BogusExpressGateway < BogusGateway
|
10
|
+
|
11
|
+
# override default purchase
|
12
|
+
def purchase amount, options
|
13
|
+
raise "amount required" if amount.nil?
|
14
|
+
raise "options required" if options.nil? or options.empty?
|
15
|
+
|
16
|
+
formatted_amount = sprintf '%.2f', (amount.to_f / 100.0)
|
17
|
+
|
18
|
+
yml = "--- !ruby/object:ActiveMerchant::Billing::PaypalExpressResponse \nauthorization: 4GN164705L1464005\navs_result: \n code: \n postal_match: \n street_match: \n message: \ncvv_result: \n code: \n message: \nfraud_review: false\nmessage: Success\nparams: \n payment_status: Completed\n tax_amount_currency_id: USD\n correlation_id: 820981d27a38f\n timestamp: \"2010-01-21T04:33:37Z\"\n pending_reason: none\n token: EC-9UM24360U0340274A\n transaction_id: 4GN164705L1464005\n fee_amount_currency_id: USD\n transaction_type: express-checkout\n build: \"1152253\"\n tax_amount: \"0.00\"\n version: \"52.0\"\n receipt_id: \n gross_amount_currency_id: USD\n parent_transaction_id: \n fee_amount: \"0.73\"\n exchange_rate: \n gross_amount: \"#{ formatted_amount }\"\n payment_date: \"2010-01-21T04:33:36Z\"\n ack: Success\n reason_code: none\n payment_type: instant\nsuccess: true\ntest: true\n"
|
19
|
+
|
20
|
+
YAML.load(yml)
|
21
|
+
end
|
22
|
+
|
23
|
+
def setup_purchase amount, options
|
24
|
+
raise "amount required" if amount.nil?
|
25
|
+
raise "options required" if options.nil? or options.empty?
|
26
|
+
OpenStruct.new :token => '123'
|
27
|
+
end
|
28
|
+
|
29
|
+
def redirect_url_for token
|
30
|
+
raise "token required" if token.nil?
|
31
|
+
'http://www.some-express-gateway-url/'
|
32
|
+
end
|
33
|
+
|
34
|
+
def details_for token
|
35
|
+
raise "token required" if token.nil?
|
36
|
+
OpenStruct.new :payer_id => '1'
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
data/lib/rack/payment.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../rack-payment'
|
@@ -0,0 +1 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../../rack-payment/test'
|
metadata
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rack-payment
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- remi
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-01-24 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: active_merchant
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
version:
|
25
|
+
description: Turn-key E-Commerce for Ruby web applications
|
26
|
+
email: remi@remitaylor.com
|
27
|
+
executables: []
|
28
|
+
|
29
|
+
extensions: []
|
30
|
+
|
31
|
+
extra_rdoc_files: []
|
32
|
+
|
33
|
+
files:
|
34
|
+
- lib/rack-payment/credit_card.rb
|
35
|
+
- lib/rack-payment/request.rb
|
36
|
+
- lib/rack-payment/test.rb
|
37
|
+
- lib/rack-payment/payment.rb
|
38
|
+
- lib/rack-payment/response.rb
|
39
|
+
- lib/rack-payment/helper.rb
|
40
|
+
- lib/rack-payment/billing_address.rb
|
41
|
+
- lib/rack-payment/methods.rb
|
42
|
+
- lib/rack-payment.rb
|
43
|
+
- lib/rack/payment.rb
|
44
|
+
- lib/rack/payment/test.rb
|
45
|
+
has_rdoc: true
|
46
|
+
homepage: http://github.com/devfu/rack-payment
|
47
|
+
licenses: []
|
48
|
+
|
49
|
+
post_install_message:
|
50
|
+
rdoc_options: []
|
51
|
+
|
52
|
+
require_paths:
|
53
|
+
- lib
|
54
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: "0"
|
59
|
+
version:
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: "0"
|
65
|
+
version:
|
66
|
+
requirements: []
|
67
|
+
|
68
|
+
rubyforge_project:
|
69
|
+
rubygems_version: 1.3.5
|
70
|
+
signing_key:
|
71
|
+
specification_version: 3
|
72
|
+
summary: Turn-key E-Commerce for Ruby web applications
|
73
|
+
test_files: []
|
74
|
+
|