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.
@@ -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
@@ -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
+