stripe-rails 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -14,6 +14,7 @@ rdoc
14
14
  spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
+ test/dummy/**/*.log
17
18
  tmp
18
19
  .rvmrc
19
20
  .DS_Store
@@ -1,3 +1,9 @@
1
+ ## 0.2.0 (2012-12-13)
2
+
3
+ * out of the box support for webhooks and critical/non-critical event handlers
4
+ * add :only guards for which webhooks you respond to-
5
+ * move stripe.js out of asset pipeline, and insert it with utility functions
6
+
1
7
  ## 0.1.0 (2012-11-14)
2
8
 
3
9
  * add config/stripe/plans.rb to define and create plans
data/README.md CHANGED
@@ -1,6 +1,11 @@
1
- # Stripe::Rails: A Full-Featured Rails Engine for Use with stripe.com
1
+ # Stripe::Rails: A Rails Engine for use with [stripe.com](https://stripe.com)
2
2
 
3
- [stripe.com](http://stripe.com) integration for your rails application
3
+ This gem can help your rails application integrate with Stripe in the following ways
4
+
5
+ * manage stripe configurations in a single place.
6
+ * makes stripe.js available from the asset pipeline.
7
+ * manage plans and coupons from within your app.
8
+ * painlessly receive and validate webhooks from stripe.
4
9
 
5
10
  ## Installation
6
11
 
@@ -9,9 +14,23 @@ Add this line to your application's Gemfile:
9
14
  gem 'stripe-rails'
10
15
 
11
16
  If you are going to be using [stripe.js][1] to securely collect credit card information
12
- on the client, then add the following to app/assets/javascripts/application.js
17
+ on the client, then you will need to add the stripe javascript tags into your template.
18
+ stripe-rails provides a helper to make this easy:
19
+
20
+ <%= stripe_javascript_tag %>
21
+
22
+ or, you can render it as a partial:
23
+
24
+ <%= render :partial => 'stripe/js' %>
13
25
 
14
- //= require stripe
26
+ In both cases, stripe-rails will choose a version of stripe.js appropriate for your
27
+ development environment and automatically configure it to use
28
+ your publishable API key. By default it uses `stripe-debug.js` for your `development`
29
+ environment and `stripe.js` for everything else, but you can manually configure it
30
+ per environment
31
+
32
+ config.stripe.debug_js = true # use stripe-debug.js
33
+ config.stripe.debug_js = false # use stripe.js
15
34
 
16
35
  ### Setup your API keys.
17
36
 
@@ -20,7 +39,7 @@ using [your api key][1]. There are two methods to do this, you can either set th
20
39
  variable `STRIPE_API_KEY`, or use the rails configuration setting `config.stripe.api_key`.
21
40
  In either case, it is recommended that you *not* check in this value into source control.
22
41
 
23
- Once you can verify that your api is set up and functioning properly by running the following command:
42
+ You can verify that your api is set up and functioning properly by running the following command:
24
43
 
25
44
  rake stripe:verify
26
45
 
@@ -83,7 +102,157 @@ plans that do, so you can run this command safely as many times as you wish.
83
102
 
84
103
  NOTE: You must destroy plans manually from your stripe dashboard.
85
104
 
86
- ## Usage
105
+ ## Webhooks
106
+
107
+ Stripe::Rails automatically sets up your application to receive webhooks from stripe.com whenever
108
+ an payment event is generated. To enable this, you will need to configure your [stripe webooks][3] to
109
+ point back to your application. By default, the webhook controller is mounted at '/stripe/events' so
110
+ you would want to enter in `http://myproductionapp.com/stripe/events` as your url for live mode,
111
+ and `http://mystagingapp.com/stripe/events` for your test mode.
112
+
113
+ If you want to mount the stripe engine somewhere else, you can do so by setting the `stripe.endpoint`
114
+ parameter. E.g.
115
+
116
+ config.stripe.endpoint = '/payment/stripe-integration'
117
+
118
+ Your new webook URL would then be `http://myproductionapp/payment/strip-integration/events`
119
+
120
+ ### Responding to webhooks
121
+
122
+ Once you have your webhook URL configured you can respond to a stripe webhook *anywhere* in your
123
+ application just by including the Stripe::Callbacks module into your class and declaring a
124
+ callback with one of the callback methods. For example, to update a customer's payment status:
125
+
126
+ class User < ActiveRecord::Base
127
+ include Stripe::Callbacks
128
+
129
+ after_customer_updated! do |customer, event|
130
+ user = User.find_by_stripe_customer_id(customer.id)
131
+ if customer.delinquent
132
+ user.is_account_current = false
133
+ user.save!
134
+ end
135
+ end
136
+ end
137
+
138
+ or to send an email with one of your customer's monthly invoices
139
+
140
+ class InvoiceMailer < ActionMailer::Base
141
+ include Stripe::Callbacks
142
+
143
+ after_invoice_created! do |invoice, event|
144
+ user = User.find_by_stripe_customer(invoice.customer)
145
+ new_invoice(user, invoice).deliver
146
+ end
147
+
148
+ def new_invoice(user, invoice)
149
+ @user = user
150
+ @invoice = invoice
151
+ mail :to => user.email, :subject => '[Acme.com] Your new invoice'
152
+ end
153
+ end
154
+
155
+ The naming convention for the callback events is after__{callback_name}! where `callback_name`
156
+ is name of the stripe event with all `.` characters substituted with underscores. So, for
157
+ example, the stripe event `customer.discount.created` can be hooked by `after_customer_discount_created!`
158
+ and so on...
159
+
160
+ Each web hook is passed an instance of the stripe object to which the event corresponds
161
+ ([`Stripe::Customer`][8], [`Stripe::Invoice`][9], [`Stripe::Charge`][10], etc...) as well as the [`Stripe::Event`][4] which contains metadata about the event being raised.
162
+
163
+ By default, the event is re-fetched securely from stripe.com to prevent damage to your system by
164
+ a malicious system spoofing real stripe events.
165
+
166
+ ### Critical and non-critical hooks
167
+
168
+ So far, the examples have all used critical hooks, but in fact, each callback method comes in two flavors: "critical",
169
+ specified with a trailing `!` character, and "non-critical", which has no "bang" character at all. What
170
+ distinguishes one from the other is that _if an exception is raised in a critical callback, it will cause the entire webhook to fail_.
171
+
172
+ This will indicate to stripe.com that you did not receive the webhook at all, and that it should retry it again later until it
173
+ receives a successful response. On the other hand, there are some tasks that are more tangential to the payment work flow and aren't
174
+ such a big deal if they get dropped on the floor. For example, A non-critical hook can be used to do things like have a bot
175
+ notify your company's chatroom that something a credit card was successfully charged:
176
+
177
+ class AcmeBot
178
+ include Stripe::Callbacks
179
+
180
+ after_charge_succeeded do |charge|
181
+ announce "Attention all Dudes and Dudettes. Ya'll are so PAID!!!"
182
+ end
183
+ end
184
+
185
+ Chances are that if you experience a momentary failure in connectivity to your chatroom, you don't want the whole payment notification to fail.
186
+
187
+
188
+ ### Filtering Callbacks
189
+
190
+ Certain stripe events represent updates to existing data. You may want to only fire the event when certain attributes of that data
191
+ are updated. You can pass an `:only` option to your callback to filter to specify which attribute updates you're interested in. For
192
+ example, to warn users whenever their credit card has changed:
193
+
194
+ class StripeMailer
195
+ include Stripe::Callbacks
196
+
197
+ after_customer_updated! :only => :active_card do |customer, evt|
198
+ your_credit_card_on_file_was_updated_are_you_sure_this_was_you(customer).deliver
199
+ end
200
+ end
201
+
202
+
203
+ Filters can be specified as an array as well:
204
+
205
+ module Accounting
206
+ include Stripe::Callbacks
207
+
208
+ after_invoice_updated! :only => [:amount, :subtotal] do
209
+ # update our records
210
+ end
211
+ end
212
+
213
+
214
+ Alternatively, you can just pass a proc to filter the event manually. It will receive an instance of [`Stripe::Event`][4] as
215
+ its parameter:
216
+
217
+ module StagingOnly
218
+ include Stripe::Callbacks
219
+
220
+ after_charge_created! :only => proc {|charge, evt| unless evt.livemode} do |charge|
221
+ puts "FAKE DATA, PLEASE IGNORE!"
222
+ end
223
+ end
224
+
225
+
226
+ ### Catchall Callback
227
+
228
+ The special 'stripe.event' callback will be invoked for every single event received from stripe.com. This can be useful for things
229
+ like logging and analytics:
230
+
231
+ class StripeFirehose
232
+ include Stripe::Callbacks
233
+
234
+ after_stripe_event do |target, event|
235
+ # do something useful
236
+ end
237
+ end
238
+
239
+
240
+ See the [complete listing of all stripe events][5], and the [webhook tutorial][6] for more great information on this subject.
241
+
242
+ ## Thanks
243
+
244
+ <a href="http://thefrontside.net">![The Frontside](http://github.com/cowboyd/therubyracer/raw/master/thefrontside.png)</a>
245
+
246
+ `Stripe::Rails` was developed fondly by your friends at [The FrontSide][7]. They are available for your custom software development
247
+ needs, including integration with stripe.com.
87
248
 
88
249
  [1]: https://stripe.com/docs/stripe.js
89
- [2]: https://manage.stripe.com/#account/apikeys
250
+ [2]: https://manage.stripe.com/#account/apikeys
251
+ [3]: https://manage.stripe.com/#account/webhooks
252
+ [4]: https://stripe.com/docs/api?lang=ruby#events
253
+ [5]: https://stripe.com/docs/api?lang=ruby#event_types
254
+ [6]: https://stripe.com/docs/webhooks
255
+ [7]: http://thefrontside.net
256
+ [8]: https://stripe.com/docs/api?lang=ruby#customers
257
+ [9]: https://stripe.com/docs/api?lang=ruby#invoices
258
+ [10]: https://stripe.com/docs/api?lang=ruby#charges
@@ -0,0 +1,5 @@
1
+ module Stripe
2
+ class ApplicationController < ActionController::Base
3
+ # is anything stripe wide?
4
+ end
5
+ end
@@ -0,0 +1,11 @@
1
+ module Stripe
2
+ class EventsController < ::Stripe::ApplicationController
3
+ include Stripe::EventDispatch
4
+ respond_to :json
5
+
6
+ def create
7
+ @event = dispatch_stripe_event params
8
+ respond_with @event, :location => nil
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,10 @@
1
+ module Stripe
2
+ class PingsController < ::Stripe::ApplicationController
3
+ respond_to :json
4
+
5
+ def show
6
+ @ping = Ping.new
7
+ respond_with @ping
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,7 @@
1
+ module Stripe
2
+ module JavascriptHelper
3
+ def stripe_javascript_tag
4
+ render :partial => 'stripe/js'
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,20 @@
1
+ require 'stripe/event'
2
+ module Stripe
3
+ module EventDispatch
4
+ def dispatch_stripe_event(params)
5
+ retrieve_stripe_event(params) do |evt|
6
+ target = evt.data.object
7
+ ::Stripe::Callbacks.run_callbacks(evt, target)
8
+ end
9
+ end
10
+
11
+ def retrieve_stripe_event(params)
12
+ id = params['id']
13
+ if id == 'evt_00000000000000' #this is a webhook test
14
+ yield Stripe::Event.construct_from(params)
15
+ else
16
+ yield Stripe::Event.retrieve(id)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,9 @@
1
+ module Stripe
2
+ class Ping
3
+ attr_reader :message
4
+
5
+ def initialize
6
+ @message = "Your sound card works perfectly!"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,8 @@
1
+ <% if Rails.application.config.stripe.debug_js %>
2
+ <script type="text/javascript" src="https://js.stripe.com/v1/stripe-debug.js"></script>
3
+ <% else %>
4
+ <script type="text/javascript" src="https://js.stripe.com/v1/"></script>
5
+ <% end %>
6
+ <script type="text/javascript">
7
+ Stripe.setPublishableKey("<%= Rails.application.config.stripe.publishable_key or fail 'No stripe.com publishable key found. Please set config.stripe.publishable_key in config/application.rb to one of your publishable keys, which can be found here: https://manage.stripe.com/#account/apikeys' %>")
8
+ </script>
@@ -0,0 +1,8 @@
1
+ Rails.application.routes.draw do
2
+ mount Stripe::Engine => Rails.application.config.stripe.endpoint
3
+ end
4
+
5
+ Stripe::Engine.routes.draw do
6
+ resource :ping, :only => :show
7
+ resources :events, :only => [:create]
8
+ end
@@ -1,4 +1,2 @@
1
- require "stripe-rails/version"
2
- require 'stripe-rails/engine'
3
- require 'stripe-rails/plans'
1
+ require 'stripe/rails'
4
2
 
@@ -0,0 +1,73 @@
1
+ require 'stripe/callbacks/builder'
2
+
3
+ module Stripe
4
+ module Callbacks
5
+ include Callbacks::Builder
6
+
7
+ callback 'account.updated'
8
+ callback 'account.application.deauthorized'
9
+ callback 'charge.succeeded'
10
+ callback 'charge.failed'
11
+ callback 'charge.refunded'
12
+ callback 'charge.dispute.created'
13
+ callback 'charge.dispute.updated'
14
+ callback 'charge.dispute.closed'
15
+ callback 'customer.created'
16
+ callback 'customer.updated'
17
+ callback 'customer.deleted'
18
+ callback 'customer.subscription.created'
19
+ callback 'customer.subscription.updated'
20
+ callback 'customer.subscription.deleted'
21
+ callback 'customer.subscription.trial_will_end'
22
+ callback 'customer.discount.created'
23
+ callback 'customer.discount.updated'
24
+ callback 'customer.discount.deleted'
25
+ callback 'invoice.created'
26
+ callback 'invoice.updated'
27
+ callback 'invoice.payment_succeeded'
28
+ callback 'invoice.payment_failed'
29
+ callback 'invoiceitem.created'
30
+ callback 'invoiceitem.updated'
31
+ callback 'invoiceitem.deleted'
32
+ callback 'plan.created'
33
+ callback 'plan.updated'
34
+ callback 'plan.deleted'
35
+ callback 'coupon.created'
36
+ callback 'coupon.updated'
37
+ callback 'coupon.deleted'
38
+ callback 'transfer.created'
39
+ callback 'transfer.updated'
40
+ callback 'transfer.failed'
41
+ callback 'ping'
42
+ callback 'stripe.event'
43
+
44
+ class << self
45
+ def run_callbacks(evt, target)
46
+ _run_callbacks evt.type, evt, target
47
+ _run_callbacks 'stripe.event', evt, target
48
+ end
49
+
50
+ def _run_callbacks(type, evt, target)
51
+ run_critical_callbacks type, evt, target
52
+ run_noncritical_callbacks type, evt, target
53
+ end
54
+
55
+ def run_critical_callbacks(type, evt, target)
56
+ ::Stripe::Callbacks::critical_callbacks[type].each do |callback|
57
+ callback.call(target, evt)
58
+ end
59
+ end
60
+
61
+ def run_noncritical_callbacks(type, evt, target)
62
+ ::Stripe::Callbacks::noncritical_callbacks[type].each do |callback|
63
+ begin
64
+ callback.call(target, evt)
65
+ rescue Exception => e
66
+ ::Rails.logger.error e.message
67
+ ::Rails.logger.error e.backtrace.join("\n")
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,58 @@
1
+ module Stripe
2
+ module Callbacks
3
+ module Builder
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ extend ActiveSupport::Concern
8
+ @critical_callbacks = Hash.new do |h, k|
9
+ h[k] = []
10
+ end
11
+ @noncritical_callbacks = Hash.new do |h, k|
12
+ h[k] = []
13
+ end
14
+ module ClassMethods
15
+ end
16
+
17
+ class << self
18
+ attr_reader :critical_callbacks, :noncritical_callbacks
19
+
20
+ def clear_callbacks!
21
+ critical_callbacks.clear
22
+ noncritical_callbacks.clear
23
+ end
24
+
25
+ def callback(name)
26
+ method_name = "after_#{name.gsub('.', '_')}"
27
+
28
+ self::ClassMethods.send(:define_method, method_name) do |options = {}, &block|
29
+ ::Stripe::Callbacks::noncritical_callbacks[name] << ::Stripe::Callbacks.callback_matcher(options, block)
30
+ end
31
+ self::ClassMethods.send(:define_method, "#{method_name}!") do |options = {}, &block|
32
+ ::Stripe::Callbacks::critical_callbacks[name] << ::Stripe::Callbacks.callback_matcher(options, block)
33
+ end
34
+ end
35
+
36
+ def callback_matcher(options, block)
37
+ case only = options[:only]
38
+ when Proc, Method
39
+ proc do |target, evt|
40
+ block.call(target, evt) if only.call(target, evt)
41
+ end
42
+ when Array, Set
43
+ stringified_keys = only.map(&:to_s)
44
+ proc do |target, evt|
45
+ intersection = evt.data.previous_attributes.keys - stringified_keys
46
+ block.call(target, evt) if intersection != evt.data.previous_attributes.keys
47
+ end
48
+ when nil
49
+ block
50
+ else
51
+ callback_matcher options.merge(:only => [only]), block
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -1,16 +1,22 @@
1
1
  require 'stripe'
2
2
 
3
- module Stripe::Rails
4
- class << self
5
- attr_accessor :testing
6
- end
7
-
3
+ module Stripe
8
4
  class Engine < ::Rails::Engine
5
+ isolate_namespace Stripe
6
+
7
+ class << self
8
+ attr_accessor :testing
9
+ end
9
10
 
10
- config.stripe = Struct.new(:api_base, :api_key, :verify_ssl_certs, :publishable_key).new
11
+ config.stripe = Struct.new(:api_base, :api_key, :verify_ssl_certs, :publishable_key, :endpoint, :debug_js).new
11
12
 
12
- initializer 'stripe.configure.api_key', :before => 'stripe.configure' do |app|
13
- app.config.stripe.api_key ||= ENV['STRIPE_API_KEY']
13
+ initializer 'stripe.configure.defaults', :before => 'stripe.configure' do |app|
14
+ stripe = app.config.stripe
15
+ stripe.api_key ||= ENV['STRIPE_API_KEY']
16
+ stripe.endpoint ||= '/stripe'
17
+ if stripe.debug_js.nil?
18
+ stripe.debug_js = ::Rails.env.development?
19
+ end
14
20
  end
15
21
 
16
22
  initializer 'stripe.configure' do |app|
@@ -26,13 +32,19 @@ environment file directly.
26
32
  MSG
27
33
  end
28
34
 
35
+ initializer 'stripe.javascript_helper' do
36
+ ActiveSupport.on_load :action_controller do
37
+ helper Stripe::JavascriptHelper
38
+ end
39
+ end
40
+
29
41
  initializer 'stripe.plans' do |app|
30
42
  path = app.root.join('config/stripe/plans.rb')
31
43
  load path if path.exist?
32
44
  end
33
45
 
34
46
  rake_tasks do
35
- load 'stripe-rails/tasks.rake'
47
+ load 'stripe/rails/tasks.rake'
36
48
  end
37
49
  end
38
50
  end
@@ -57,7 +57,7 @@ module Stripe
57
57
 
58
58
  def put!
59
59
  if exists?
60
- puts "[EXISTS] - #{@id}" unless Stripe::Rails.testing
60
+ puts "[EXISTS] - #{@id}" unless Stripe::Engine.testing
61
61
  else
62
62
  plan = Stripe::Plan.create(
63
63
  :id => @id,
@@ -68,7 +68,7 @@ module Stripe
68
68
  :interval_count => @interval_count,
69
69
  :trial_period_days => @trial_period_days
70
70
  )
71
- puts "[CREATE] - #{plan}" unless Stripe::Rails.testing
71
+ puts "[CREATE] - #{plan}" unless Stripe::Engine.testing
72
72
  end
73
73
  end
74
74
 
@@ -0,0 +1,4 @@
1
+ require "stripe/rails/version"
2
+ require 'stripe/engine'
3
+ require 'stripe/plans'
4
+ require 'stripe/callbacks'
@@ -11,7 +11,7 @@ namespace :stripe do
11
11
  end
12
12
  end
13
13
 
14
- task 'plans:prepare' => :environment do
14
+ task 'plans:prepare' => 'environment' do
15
15
  Stripe::Plans.put!
16
16
  end
17
17
 
@@ -1,5 +1,5 @@
1
1
  module Stripe
2
2
  module Rails
3
- VERSION = "0.1.0"
3
+ VERSION = "0.2.0"
4
4
  end
5
5
  end
@@ -1,5 +1,5 @@
1
1
  # -*- encoding: utf-8 -*-
2
- require File.expand_path('../lib/stripe-rails/version', __FILE__)
2
+ require File.expand_path('../lib/stripe/rails/version', __FILE__)
3
3
 
4
4
  Gem::Specification.new do |gem|
5
5
  gem.authors = ["rubygeek"]
@@ -14,9 +14,10 @@ Gem::Specification.new do |gem|
14
14
  gem.name = "stripe-rails"
15
15
  gem.require_paths = ["lib"]
16
16
  gem.version = Stripe::Rails::VERSION
17
- gem.add_dependency 'railties', '~> 3.0'
17
+ gem.add_dependency 'rails', '~> 3.0'
18
18
  gem.add_dependency 'stripe'
19
19
 
20
20
  gem.add_development_dependency 'tzinfo'
21
21
  gem.add_development_dependency 'mocha'
22
+ gem.add_development_dependency 'rack-test'
22
23
  end
@@ -0,0 +1,171 @@
1
+ require 'minitest/autorun'
2
+ require 'spec_helper'
3
+
4
+ describe Stripe::Callbacks do
5
+ include Rack::Test::Methods
6
+
7
+ def app
8
+ Rails.application
9
+ end
10
+
11
+ before do
12
+ header 'Accept', 'application/json'
13
+ header 'Content-Type', 'application/json'
14
+ @observer = Class.new.tap do |cls|
15
+ cls.class_eval do
16
+ include Stripe::Callbacks
17
+ end
18
+ end
19
+ @content = JSON.parse(File.read File.expand_path('../invoice_payment_succeeded.json', __FILE__))
20
+ self.type = @content['type']
21
+
22
+ end
23
+
24
+ def type=(type)
25
+ @content['type'] = type
26
+ @stubbed_event = Stripe::Event.construct_from(@content)
27
+ Stripe::Event.stubs(:retrieve).returns(@stubbed_event)
28
+ end
29
+
30
+ after do
31
+ ::Stripe::Callbacks.clear_callbacks!
32
+ end
33
+
34
+ it 'has a ping interface just to make sure that everything is working just fine' do
35
+ get '/stripe/ping'
36
+ assert last_response.ok?
37
+ end
38
+
39
+ describe 'defined with a bang' do
40
+ code = nil
41
+ before do
42
+ code = proc {|target, e| @event = e; @target = target}
43
+ @observer.class_eval do
44
+ after_invoice_payment_succeeded! do |evt, target|
45
+ code.call(evt, target)
46
+ end
47
+ end
48
+ end
49
+ it 'is invoked for the invoice.payment_succeeded event' do
50
+ post 'stripe/events', JSON.pretty_generate(@content)
51
+ @event.wont_be_nil
52
+ @event.type.must_equal 'invoice.payment_succeeded'
53
+ @target.total.must_equal 6999
54
+ end
55
+ it 'is not invoked for other types of events' do
56
+ self.type = 'invoked.payment_failed'
57
+ post 'stripe/events/', JSON.pretty_generate(@content)
58
+ end
59
+ describe 'if it raises an exception' do
60
+ before do
61
+ code = proc {fail 'boom!'}
62
+ end
63
+ it 'causes the whole webhook to fail' do
64
+ proc {post 'stripe/events', JSON.pretty_generate(@content)}.must_raise RuntimeError
65
+ end
66
+ end
67
+ end
68
+
69
+ describe 'defined without a bang and raising an exception' do
70
+ before do
71
+ @observer.class_eval do
72
+ after_invoice_payment_succeeded do |evt|
73
+ fail 'boom!'
74
+ end
75
+ end
76
+ end
77
+
78
+ it 'does not cause the webhook to fail' do
79
+ post 'stripe/events', JSON.pretty_generate(@content)
80
+ last_response.status.must_be :>=, 200
81
+ last_response.status.must_be :<, 300
82
+ end
83
+ end
84
+
85
+ describe 'designed to catch any event' do
86
+ events = nil
87
+ before do
88
+ events = []
89
+ @observer.class_eval do
90
+ after_stripe_event do |target, evt|
91
+ events << evt
92
+ end
93
+ end
94
+ end
95
+ it 'gets invoked for any standard event' do
96
+ self.type = 'invoice.payment_failed'
97
+ post 'stripe/events/', JSON.pretty_generate(@content)
98
+ events.first.type.must_equal 'invoice.payment_failed'
99
+ end
100
+
101
+ it 'gets invoked for any event whatsoever' do
102
+ self.type = 'foo.bar.baz'
103
+ post 'stripe/events/', JSON.pretty_generate(@content)
104
+ events.first.type.must_equal 'foo.bar.baz'
105
+ end
106
+ end
107
+
108
+ describe 'filtering on specific changed attributes' do
109
+ events = nil
110
+ before do
111
+ events = []
112
+ self.type = 'invoice.updated'
113
+ @stubbed_event.data.previous_attributes = {}
114
+ end
115
+ describe 'specified as an single symbol' do
116
+ before do
117
+ @observer.class_eval do
118
+ after_invoice_updated! :only => :closed do |invoice, evt|
119
+ events << evt
120
+ end
121
+ end
122
+ end
123
+ it 'does not fire events for with a prior attribute was specified' do
124
+ post 'stripe/events', JSON.pretty_generate(@content)
125
+ events.length.must_equal 0
126
+ end
127
+ it 'does fire events for which the prior attribute was specified' do
128
+ @stubbed_event.data.previous_attributes['closed'] = true
129
+ post 'stripe/events', JSON.pretty_generate(@content)
130
+ events.length.must_equal 1
131
+ end
132
+ end
133
+ describe 'specified as an array' do
134
+ before do
135
+ @observer.class_eval do
136
+ after_invoice_updated! :only => [:currency, :subtotal] do |invoice, evt|
137
+ events << evt
138
+ end
139
+ end
140
+ end
141
+ it 'does not fire events for which prior attributes were not specified' do
142
+ post 'stripe/events', JSON.pretty_generate(@content)
143
+ events.length.must_equal 0
144
+ end
145
+ it 'does fire events for which prior attributes were specified' do
146
+ @stubbed_event.data.previous_attributes['subtotal'] = 699
147
+ post 'stripe/events', JSON.pretty_generate(@content)
148
+ events.length.must_equal 1
149
+ end
150
+ end
151
+ describe 'specified as a lambda' do
152
+ before do
153
+ @observer.class_eval do
154
+ after_invoice_updated :only => proc {|target, evt| evt.data.previous_attributes.has_key? "closed"} do |i,e|
155
+ events << e
156
+ end
157
+ end
158
+ end
159
+ it 'does not fire events for which the lambda is not true' do
160
+ post 'stripe/events', JSON.pretty_generate(@content)
161
+ events.length.must_equal 0
162
+ end
163
+
164
+ it 'does fire events for when the lambda is true' do
165
+ @stubbed_event.data.previous_attributes['closed'] = 'false'
166
+ post 'stripe/events', JSON.pretty_generate(@content)
167
+ events.length.must_equal 1
168
+ end
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,65 @@
1
+ {
2
+ "type": "invoice.payment_succeeded",
3
+ "livemode": false,
4
+ "pending_webhooks": 0,
5
+ "created": 1352910508,
6
+ "object": "event",
7
+ "id": "evt_0jcGFxHNsrDTj4",
8
+ "data": {
9
+ "object": {
10
+ "starting_balance": 0,
11
+ "ending_balance": 0,
12
+ "paid": true,
13
+ "attempted": true,
14
+ "total": 6999,
15
+ "lines": {
16
+ "object": "list",
17
+ "count": 1,
18
+ "data": [
19
+ {
20
+ "type": "subscription",
21
+ "plan": {
22
+ "livemode": false,
23
+ "object": "plan",
24
+ "interval_count": 1,
25
+ "amount": 6999,
26
+ "name": "BandFrame PRO Annual",
27
+ "currency": "usd",
28
+ "id": "yearly",
29
+ "interval": "year",
30
+ "trial_period_days": null
31
+ },
32
+ "livemode": false,
33
+ "object": "line_item",
34
+ "description": null,
35
+ "amount": 6999,
36
+ "currency": "usd",
37
+ "period": {
38
+ "end": 1384446507,
39
+ "start": 1352910507
40
+ },
41
+ "id": "su_0jcGpDYAkeNj0a",
42
+ "proration": false,
43
+ "quantity": 1
44
+ }
45
+ ],
46
+ "url": "/v1/invoices/in_0jcG7zz2RtscPB/lines"
47
+ },
48
+ "subtotal": 6999,
49
+ "livemode": false,
50
+ "period_start": 1352910503,
51
+ "charge": "ch_0jcGRmThyuRYyh",
52
+ "object": "invoice",
53
+ "customer": "cus_0jcFnB3UIIiicv",
54
+ "period_end": 1352910507,
55
+ "discount": null,
56
+ "next_payment_attempt": null,
57
+ "currency": "usd",
58
+ "date": 1352910507,
59
+ "closed": true,
60
+ "id": "in_0jcG7zz2RtscPB",
61
+ "attempt_count": 0,
62
+ "amount_due": 6999
63
+ }
64
+ }
65
+ }
@@ -15,5 +15,5 @@ if ActiveSupport::TestCase.method_defined?(:fixture_path=)
15
15
  ActiveSupport::TestCase.fixture_path = File.expand_path("../fixtures", __FILE__)
16
16
  end
17
17
 
18
- Stripe::Rails.testing = true
18
+ Stripe::Engine.testing = true
19
19
  require 'mocha/setup'
@@ -11,7 +11,7 @@ end
11
11
 
12
12
  describe 'initializing plans' do
13
13
  require 'rake'
14
- load 'stripe-rails/tasks.rake'
14
+ load 'stripe/rails/tasks.rake'
15
15
  it 'creates any plans that do not exist on stripe.com' do
16
16
  Stripe::Plans.expects(:put!)
17
17
  Rake::Task['stripe:plans:prepare'].invoke
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stripe-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,10 +9,10 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-11-14 00:00:00.000000000 Z
12
+ date: 2012-12-13 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
- name: railties
15
+ name: rails
16
16
  requirement: !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
@@ -75,6 +75,22 @@ dependencies:
75
75
  - - ! '>='
76
76
  - !ruby/object:Gem::Version
77
77
  version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: rack-test
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
78
94
  description: A gem to integrate stripe into your rails app
79
95
  email:
80
96
  - nola@rubygeek.com
@@ -88,15 +104,27 @@ files:
88
104
  - LICENSE
89
105
  - README.md
90
106
  - Rakefile
107
+ - app/controllers/stripe/application_controller.rb
108
+ - app/controllers/stripe/events_controller.rb
109
+ - app/controllers/stripe/pings_controller.rb
110
+ - app/helpers/stripe/javascript_helper.rb
111
+ - app/models/stripe/event_dispatch.rb
112
+ - app/models/stripe/ping.rb
113
+ - app/views/stripe/_js.html.erb
114
+ - config/routes.rb
91
115
  - lib/generators/stripe/install_generator.rb
92
116
  - lib/generators/templates/plans.rb
93
117
  - lib/stripe-rails.rb
94
- - lib/stripe-rails/engine.rb
95
- - lib/stripe-rails/plans.rb
96
- - lib/stripe-rails/tasks.rake
97
- - lib/stripe-rails/version.rb
118
+ - lib/stripe/callbacks.rb
119
+ - lib/stripe/callbacks/builder.rb
120
+ - lib/stripe/engine.rb
121
+ - lib/stripe/plans.rb
122
+ - lib/stripe/rails.rb
123
+ - lib/stripe/rails/tasks.rake
124
+ - lib/stripe/rails/version.rb
98
125
  - stripe-rails.gemspec
99
126
  - test/.DS_Store
127
+ - test/callbacks_spec.rb
100
128
  - test/dummy/README.rdoc
101
129
  - test/dummy/Rakefile
102
130
  - test/dummy/app/assets/javascripts/application.js
@@ -125,17 +153,15 @@ files:
125
153
  - test/dummy/config/stripe/plans.rb
126
154
  - test/dummy/lib/assets/.gitkeep
127
155
  - test/dummy/log/.gitkeep
128
- - test/dummy/log/test.log
129
156
  - test/dummy/public/404.html
130
157
  - test/dummy/public/422.html
131
158
  - test/dummy/public/500.html
132
159
  - test/dummy/public/favicon.ico
133
160
  - test/dummy/script/rails
161
+ - test/invoice_payment_succeeded.json
134
162
  - test/plan_builder_spec.rb
135
163
  - test/spec_helper.rb
136
164
  - test/stripe_rails_spec.rb
137
- - vendor/assets/javascripts/stripe-debug.js
138
- - vendor/assets/javascripts/stripe.js.erb
139
165
  homepage: ''
140
166
  licenses: []
141
167
  post_install_message:
@@ -162,6 +188,7 @@ specification_version: 3
162
188
  summary: A gem to integrate stripe into your rails app
163
189
  test_files:
164
190
  - test/.DS_Store
191
+ - test/callbacks_spec.rb
165
192
  - test/dummy/README.rdoc
166
193
  - test/dummy/Rakefile
167
194
  - test/dummy/app/assets/javascripts/application.js
@@ -190,12 +217,12 @@ test_files:
190
217
  - test/dummy/config/stripe/plans.rb
191
218
  - test/dummy/lib/assets/.gitkeep
192
219
  - test/dummy/log/.gitkeep
193
- - test/dummy/log/test.log
194
220
  - test/dummy/public/404.html
195
221
  - test/dummy/public/422.html
196
222
  - test/dummy/public/500.html
197
223
  - test/dummy/public/favicon.ico
198
224
  - test/dummy/script/rails
225
+ - test/invoice_payment_succeeded.json
199
226
  - test/plan_builder_spec.rb
200
227
  - test/spec_helper.rb
201
228
  - test/stripe_rails_spec.rb
File without changes
@@ -1,279 +0,0 @@
1
- (function() {
2
- var _this = this;
3
-
4
- this.Stripe = (function() {
5
-
6
- function Stripe() {}
7
-
8
- Stripe.version = 2;
9
-
10
- Stripe.endpoint = 'https://api.stripe.com/v1';
11
-
12
- Stripe.validateCardNumber = function(num) {
13
- num = (num + '').replace(/\s+|-/g, '');
14
- return num.length >= 10 && num.length <= 16 && Stripe.luhnCheck(num);
15
- };
16
-
17
- Stripe.validateCVC = function(num) {
18
- num = Stripe.trim(num);
19
- return /^\d+$/.test(num) && num.length >= 3 && num.length <= 4;
20
- };
21
-
22
- Stripe.validateExpiry = function(month, year) {
23
- var currentTime, expiry;
24
- month = Stripe.trim(month);
25
- year = Stripe.trim(year);
26
- if (!/^\d+$/.test(month)) {
27
- return false;
28
- }
29
- if (!/^\d+$/.test(year)) {
30
- return false;
31
- }
32
- if (!(parseInt(month, 10) <= 12)) {
33
- return false;
34
- }
35
- expiry = new Date(year, month);
36
- currentTime = new Date;
37
- expiry.setMonth(expiry.getMonth() - 1);
38
- expiry.setMonth(expiry.getMonth() + 1, 1);
39
- return expiry > currentTime;
40
- };
41
-
42
- Stripe.cardType = function(num) {
43
- return Stripe.cardTypes[num.slice(0, 2)] || 'Unknown';
44
- };
45
-
46
- Stripe.setPublishableKey = function(key) {
47
- Stripe.key = key;
48
- };
49
-
50
- Stripe.createToken = function(card, params, callback) {
51
- var amount, key, value;
52
- if (params == null) {
53
- params = {};
54
- }
55
- if (!card) {
56
- throw 'card required';
57
- }
58
- if (typeof card !== 'object') {
59
- throw 'card invalid';
60
- }
61
- if (typeof params === 'function') {
62
- callback = params;
63
- params = {};
64
- } else if (typeof params !== 'object') {
65
- amount = parseInt(params, 10);
66
- params = {};
67
- if (amount > 0) {
68
- params.amount = amount;
69
- }
70
- }
71
- for (key in card) {
72
- value = card[key];
73
- delete card[key];
74
- card[Stripe.underscore(key)] = value;
75
- }
76
- params.card = card;
77
- params.key || (params.key = Stripe.key || Stripe.publishableKey);
78
- Stripe.validateKey(params.key);
79
- return Stripe.ajaxJSONP({
80
- url: "" + Stripe.endpoint + "/tokens",
81
- data: params,
82
- method: 'POST',
83
- success: function(body, status) {
84
- return typeof callback === "function" ? callback(status, body) : void 0;
85
- },
86
- complete: Stripe.complete(callback),
87
- timeout: 40000
88
- });
89
- };
90
-
91
- Stripe.getToken = function(token, callback) {
92
- if (!token) {
93
- throw 'token required';
94
- }
95
- Stripe.validateKey(Stripe.key);
96
- return Stripe.ajaxJSONP({
97
- url: "" + Stripe.endpoint + "/tokens/" + token,
98
- data: {
99
- key: Stripe.key
100
- },
101
- success: function(body, status) {
102
- return typeof callback === "function" ? callback(status, body) : void 0;
103
- },
104
- complete: Stripe.complete(callback),
105
- timeout: 40000
106
- });
107
- };
108
-
109
- Stripe.complete = function(callback) {
110
- return function(type, xhr, options) {
111
- if (type !== 'success') {
112
- return typeof callback === "function" ? callback(500, {
113
- error: {
114
- code: type,
115
- type: type,
116
- message: 'An unexpected error has occured.\nWe have been notified of the problem.'
117
- }
118
- }) : void 0;
119
- }
120
- };
121
- };
122
-
123
- Stripe.validateKey = function(key) {
124
- if (!key || typeof key !== 'string') {
125
- throw new Error('You did not set a valid publishable key.\nCall Stripe.setPublishableKey() with your publishable key.\nFor more info, see https://stripe.com/docs/stripe.js');
126
- }
127
- if (/^sk_/.test(key)) {
128
- throw new Error('You are using a secret key with Stripe.js, instead of the publishable one.\nFor more info, see https://stripe.com/docs/stripe.js');
129
- }
130
- };
131
-
132
- return Stripe;
133
-
134
- }).call(this);
135
-
136
- if (typeof module !== "undefined" && module !== null) {
137
- module.exports = this.Stripe;
138
- }
139
-
140
- if (typeof define === "function") {
141
- define('stripe', [], function() {
142
- return _this.Stripe;
143
- });
144
- }
145
-
146
- }).call(this);
147
- (function() {
148
- var e, requestID, serialize,
149
- __slice = [].slice;
150
-
151
- e = encodeURIComponent;
152
-
153
- requestID = new Date().getTime();
154
-
155
- serialize = function(object, result, scope) {
156
- var key, value;
157
- if (result == null) {
158
- result = [];
159
- }
160
- for (key in object) {
161
- value = object[key];
162
- if (scope) {
163
- key = "" + scope + "[" + key + "]";
164
- }
165
- if (typeof value === 'object') {
166
- serialize(value, result, key);
167
- } else {
168
- result.push("" + key + "=" + (e(value)));
169
- }
170
- }
171
- return result.join('&').replace(/%20/g, '+');
172
- };
173
-
174
- this.Stripe.ajaxJSONP = function(options) {
175
- var abort, abortTimeout, callbackName, head, script, xhr;
176
- if (options == null) {
177
- options = {};
178
- }
179
- callbackName = 'sjsonp' + (++requestID);
180
- script = document.createElement('script');
181
- abortTimeout = null;
182
- abort = function() {
183
- var _ref;
184
- if ((_ref = script.parentNode) != null) {
185
- _ref.removeChild(script);
186
- }
187
- if (callbackName in window) {
188
- window[callbackName] = (function() {});
189
- }
190
- return typeof options.complete === "function" ? options.complete('abort', xhr, options) : void 0;
191
- };
192
- xhr = {
193
- abort: abort
194
- };
195
- script.onerror = function() {
196
- xhr.abort();
197
- return typeof options.error === "function" ? options.error(xhr, options) : void 0;
198
- };
199
- window[callbackName] = function() {
200
- var args;
201
- args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
202
- clearTimeout(abortTimeout);
203
- script.parentNode.removeChild(script);
204
- try {
205
- delete window[callbackName];
206
- } catch (e) {
207
- window[callbackName] = void 0;
208
- }
209
- if (typeof options.success === "function") {
210
- options.success.apply(options, args);
211
- }
212
- return typeof options.complete === "function" ? options.complete('success', xhr, options) : void 0;
213
- };
214
- options.data || (options.data = {});
215
- options.data.callback = callbackName;
216
- if (options.method) {
217
- options.data._method = options.method;
218
- }
219
- script.src = options.url + '?' + serialize(options.data);
220
- head = document.getElementsByTagName('head')[0];
221
- head.appendChild(script);
222
- if (options.timeout > 0) {
223
- abortTimeout = setTimeout(function() {
224
- xhr.abort();
225
- return typeof options.complete === "function" ? options.complete('timeout', xhr, options) : void 0;
226
- }, options.timeout);
227
- }
228
- return xhr;
229
- };
230
-
231
- }).call(this);
232
- (function() {
233
-
234
- this.Stripe.trim = function(str) {
235
- return (str + '').replace(/^\s+|\s+$/g, '');
236
- };
237
-
238
- this.Stripe.underscore = function(str) {
239
- return (str + '').replace(/([A-Z])/g, function($1) {
240
- return "_" + ($1.toLowerCase());
241
- });
242
- };
243
-
244
- this.Stripe.luhnCheck = function(num) {
245
- var digit, digits, odd, sum, _i, _len;
246
- odd = true;
247
- sum = 0;
248
- digits = (num + '').split('').reverse();
249
- for (_i = 0, _len = digits.length; _i < _len; _i++) {
250
- digit = digits[_i];
251
- digit = parseInt(digit, 10);
252
- if ((odd = !odd)) {
253
- digit *= 2;
254
- }
255
- if (digit > 9) {
256
- digit -= 9;
257
- }
258
- sum += digit;
259
- }
260
- return sum % 10 === 0;
261
- };
262
-
263
- this.Stripe.cardTypes = (function() {
264
- var num, types, _i, _j;
265
- types = {};
266
- for (num = _i = 40; _i <= 49; num = ++_i) {
267
- types[num] = 'Visa';
268
- }
269
- for (num = _j = 50; _j <= 59; num = ++_j) {
270
- types[num] = 'MasterCard';
271
- }
272
- types[34] = types[37] = 'American Express';
273
- types[60] = types[62] = types[64] = types[65] = 'Discover';
274
- types[35] = 'JCB';
275
- types[30] = types[36] = types[38] = types[39] = 'Diners Club';
276
- return types;
277
- })();
278
-
279
- }).call(this);
@@ -1,3 +0,0 @@
1
- //= require stripe-debug
2
-
3
- Stripe.publishableKey = "<%= Rails.application.config.stripe.publishable_key or fail 'No stripe.com publishable key found. Please set config.stripe.publishable_key in config/application.rb to one of your publishable keys, which can be found here: https://manage.stripe.com/#account/apikeys' %>"