rack-payment 0.0.1

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