rack-payment 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|