stripe-rails 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.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' %>"