subscription_fu 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. data/.gitignore +4 -0
  2. data/Gemfile +16 -0
  3. data/LICENSE +20 -0
  4. data/README.md +96 -0
  5. data/Rakefile +6 -0
  6. data/app/models/subscription_fu/plan.rb +43 -0
  7. data/app/models/subscription_fu/subscription.rb +117 -0
  8. data/app/models/subscription_fu/transaction.rb +131 -0
  9. data/config/locales/en.yml +7 -0
  10. data/examples/routes.rb +7 -0
  11. data/examples/subscriptions_controller.rb +12 -0
  12. data/examples/transactions_controller.rb +33 -0
  13. data/lib/generators/subscription_fu/install_generator.rb +23 -0
  14. data/lib/generators/subscription_fu/templates/en.yml +17 -0
  15. data/lib/generators/subscription_fu/templates/initializer.rb +24 -0
  16. data/lib/generators/subscription_fu/templates/migration.rb +38 -0
  17. data/lib/subscription_fu/config.rb +36 -0
  18. data/lib/subscription_fu/engine.rb +5 -0
  19. data/lib/subscription_fu/models.rb +54 -0
  20. data/lib/subscription_fu/paypal.rb +101 -0
  21. data/lib/subscription_fu/railtie.rb +12 -0
  22. data/lib/subscription_fu/version.rb +3 -0
  23. data/lib/subscription_fu.rb +15 -0
  24. data/spec/app/.gitignore +1 -0
  25. data/spec/app/Rakefile +8 -0
  26. data/spec/app/app/controllers/application_controller.rb +6 -0
  27. data/spec/app/app/models/initiator.rb +2 -0
  28. data/spec/app/app/models/subject.rb +4 -0
  29. data/spec/app/app/views/layouts/application.html.haml +6 -0
  30. data/spec/app/config/application.rb +11 -0
  31. data/spec/app/config/boot.rb +4 -0
  32. data/spec/app/config/database.yml +18 -0
  33. data/spec/app/config/environment.rb +5 -0
  34. data/spec/app/config/environments/development.rb +18 -0
  35. data/spec/app/config/environments/test.rb +22 -0
  36. data/spec/app/config/initializers/backtrace_silencers.rb +7 -0
  37. data/spec/app/config/initializers/inflections.rb +2 -0
  38. data/spec/app/config/initializers/secret_token.rb +2 -0
  39. data/spec/app/config/initializers/subscription_fu.rb +28 -0
  40. data/spec/app/config/locales/subscription_fu.en.yml +17 -0
  41. data/spec/app/config/routes.rb +3 -0
  42. data/spec/app/config.ru +4 -0
  43. data/spec/app/db/.gitignore +1 -0
  44. data/spec/app/db/migrate/20110516061428_create_subjects.rb +13 -0
  45. data/spec/app/db/migrate/20110516061443_create_initiators.rb +14 -0
  46. data/spec/app/db/migrate/20110516070948_create_subscription_fu_tables.rb +38 -0
  47. data/spec/app/db/schema.rb +62 -0
  48. data/spec/app/script/rails +10 -0
  49. data/spec/factories/initiator.rb +4 -0
  50. data/spec/factories/subject.rb +2 -0
  51. data/spec/factories/subscription.rb +6 -0
  52. data/spec/factories/transaction.rb +7 -0
  53. data/spec/models/subscription_spec.rb +277 -0
  54. data/spec/models/subscription_transaction_spec.rb +135 -0
  55. data/spec/spec_helper.rb +18 -0
  56. data/spec/support/paypal_test_helper.rb +82 -0
  57. data/subscription_fu.gemspec +23 -0
  58. metadata +134 -0
@@ -0,0 +1,277 @@
1
+ require 'spec_helper'
2
+
3
+ describe SubscriptionFu::Subscription do
4
+
5
+ class << self
6
+ def should_build_valid_successor(expected_plan_key, start_time_instance, billing_start_time_instance)
7
+ it "should be valid subscription successor" do
8
+ @succ.subject.should == @sub.subject
9
+ @succ.prev_subscription.should == @sub
10
+ @succ.plan_key.should == expected_plan_key
11
+ @succ.starts_at.should == instance_variable_get("@#{start_time_instance}")
12
+ @succ.billing_starts_at.should == instance_variable_get("@#{billing_start_time_instance}")
13
+ end
14
+ end
15
+ def should_build_valid_activation_transaction(sub_instance, trans_instance, first_sub)
16
+ it "should build valid activation transaction" do
17
+ sub = instance_variable_get("@#{sub_instance}")
18
+ trans = instance_variable_get("@#{trans_instance}")
19
+ trans.subscription.should == sub
20
+ trans.initiator.should == @initiator
21
+ trans.action.should == "activation"
22
+ trans.status.should == "initiated"
23
+ trans.identifier.should be_nil
24
+
25
+ if first_sub
26
+ trans.related_transactions.should be_empty
27
+ else
28
+ trans.related_transactions.size.should == 1
29
+ rel_trans = trans.related_transactions.first
30
+ rel_trans.subscription.should == sub.prev_subscription
31
+ rel_trans.initiator.should == @initiator
32
+ rel_trans.action.should == "cancellation"
33
+ rel_trans.status.should == "initiated"
34
+ rel_trans.identifier.should be_nil
35
+ end
36
+ end
37
+ end
38
+ def should_build_free_activation_transaction
39
+ it "should be activation transaction for free sub" do
40
+ @trans.gateway.should == "nogw"
41
+ end
42
+ end
43
+ def should_build_paypal_activation_transaction
44
+ it "should be activation transaction for paypal sub" do
45
+ @trans.gateway.should == "paypal"
46
+ end
47
+ end
48
+ def should_activate_subscription(sub_instance)
49
+ it "should activate subscription" do
50
+ sub = instance_variable_get("@#{sub_instance}").reload
51
+ sub.should be_activated
52
+ sub.transactions.first.status.should == "complete"
53
+ end
54
+ end
55
+ def should_cancel_previous_subscription(sub_instance)
56
+ it "should cancel previous sub" do
57
+ sub = instance_variable_get("@#{sub_instance}").prev_subscription.reload
58
+ sub.canceled_at.should be_present
59
+ sub.transactions.last.status.should == "complete"
60
+ end
61
+ end
62
+ def should_cancel_subscription(sub_instance)
63
+ it "should cancel sub" do
64
+ sub = instance_variable_get("@#{sub_instance}").reload
65
+ sub.canceled_at.should be_present
66
+ sub.transactions.last.status.should == "complete"
67
+ end
68
+ end
69
+ def should_have_free_activation_flow(sub_instance, first_sub)
70
+ context "activation" do
71
+ before { @trans = instance_variable_get("@#{sub_instance}").initiate_activation(@initiator) }
72
+ should_build_valid_activation_transaction(sub_instance, :trans, first_sub)
73
+ should_build_free_activation_transaction
74
+ context "authorize" do
75
+ before { @redirect_target = @trans.start_checkout("http://return.to", "http://cancel.to") }
76
+ it("should redirect to return URL") { @redirect_target.should == "http://return.to" }
77
+ it("should be pending transaction on subject") do
78
+ instance_variable_get("@#{sub_instance}").subject.pending_transaction(@trans.identifier).should == @trans
79
+ end
80
+ context "complete" do
81
+ before { mock_paypal_delete_profile("fgsga564aa") } unless first_sub
82
+ before { @trans.complete }
83
+ should_activate_subscription(sub_instance)
84
+ should_cancel_previous_subscription(sub_instance) unless first_sub
85
+ end
86
+ end
87
+ end
88
+ end
89
+ def should_have_paid_activation_flow(sub_instance, first_sub, prev_sub_is_free = false)
90
+ context "activation" do
91
+ before { @trans = instance_variable_get("@#{sub_instance}").initiate_activation(@initiator) }
92
+ should_build_valid_activation_transaction(sub_instance, :trans, first_sub)
93
+ should_build_paypal_activation_transaction
94
+ context "authorization" do
95
+ before do
96
+ mock_paypal_express_checkout
97
+ @redirect_target = @trans.start_checkout("http://return.to", "http://cancel.to")
98
+ end
99
+ it("should redirect to return URL") do
100
+ @redirect_target.should == "https://www.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token=token123"
101
+ end
102
+ it("should be pending transaction on subject") do
103
+ instance_variable_get("@#{sub_instance}").subject.pending_transaction("token123").should == @trans
104
+ end
105
+ context "complete" do
106
+ before { mock_paypal_create_profile("token123", "6bvsaksd9j") }
107
+ before { mock_paypal_delete_profile("fgsga564aa") } unless first_sub
108
+ before { @trans.complete }
109
+ should_activate_subscription(sub_instance)
110
+ should_cancel_previous_subscription(sub_instance) unless first_sub
111
+ end
112
+ context "complete with error in cancel" do
113
+ before { mock_paypal_create_profile("token123", "6bvsaksd9j") }
114
+ before { mock_paypal_delete_profile_with_error("fgsga564aa") }
115
+ before { @trans.complete }
116
+ should_activate_subscription(sub_instance)
117
+ it "should cancel previous sub with failure" do
118
+ sub = instance_variable_get("@#{sub_instance}").prev_subscription.reload
119
+ sub.canceled_at.should be_present
120
+ sub.transactions.last.status.should == "failed"
121
+ end
122
+ end unless first_sub || prev_sub_is_free
123
+ context "complete with error in create" do
124
+ before { mock_paypal_create_profile_with_error("token123") }
125
+ before { mock_paypal_delete_profile("fgsga564aa") } unless first_sub
126
+ before { @trans.complete }
127
+ it "should not activate subscription" do
128
+ sub = instance_variable_get("@#{sub_instance}").reload
129
+ sub.should_not be_activated
130
+ sub.transactions.first.status.should == "failed"
131
+ end
132
+ it "should not cancel previous subscription" do
133
+ sub = instance_variable_get("@#{sub_instance}").prev_subscription.reload
134
+ sub.canceled_at.should be_blank
135
+ sub.transactions.last.status.should == "aborted"
136
+ end unless first_sub
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
142
+
143
+ it { should belong_to :subject }
144
+ it { should have_many :transactions }
145
+ it { should validate_presence_of :subject }
146
+ it { should validate_presence_of :plan_key }
147
+ %w( free profess premium basic ).each {|p| it { should allow_value(p).for(:plan_key) } }
148
+ it { should_not allow_value("blah").for(:plan_key) }
149
+ it { should validate_presence_of :starts_at }
150
+
151
+ context "base" do
152
+ before { @initiator = Factory(:initiator) }
153
+
154
+ context "free subscription" do
155
+ before { at_time("2010-01-10 00:00 UTC") { @sub = Factory(:subscription, :plan_key => "free") } }
156
+ it("should indicate it isn't a paid subscription") { @sub.should_not be_paid_subscription }
157
+ it("should return human name") do
158
+ pending do
159
+ @sub.human_description.should == "MyApp Free subscription for Subject, 0 JPY per month"
160
+ end
161
+ end
162
+ it("should return no next_billing_date") { @sub.next_billing_date.should be_nil }
163
+ should_have_free_activation_flow(:sub, true)
164
+ context "basic successor" do
165
+ before do
166
+ @now = Time.now
167
+ @sub.update_attribute :activated_at, @now - 53.hours
168
+ at_time(@now) { @succ = @sub.subject.build_next_subscription('basic') ; @succ.save! }
169
+ end
170
+ should_build_valid_successor("basic", :now, :now)
171
+ should_have_paid_activation_flow(:succ, false, true)
172
+ end
173
+ end
174
+
175
+ context "basic subscription" do
176
+ before { @sub = Factory(:subscription, :plan_key => "basic", :paypal_profile_id => "bg5431ddf") }
177
+ it("should indicate it is a paid subscription") { @sub.should be_paid_subscription }
178
+ it("should return human name") do
179
+ pending do
180
+ @sub.human_description.should == "MyApp Basic subscription for Subject, 1050 JPY per month"
181
+ end
182
+ end
183
+ end
184
+
185
+ context "premium subscription" do
186
+ before do
187
+ @now = Time.parse("2010-01-12 11:45")
188
+ @sub_start = Time.parse("2010-01-10 00:00 UTC")
189
+ @next_billing = Time.parse("2010-02-10 00:00 UTC")
190
+ at_time(@sub_start) do
191
+ @sub = Factory(:subscription, :plan_key => "premium", :paypal_profile_id => "fgsga564aa",
192
+ :starts_at => @sub_start, :billing_starts_at => @sub_start, :activated_at => @sub_start)
193
+ end
194
+ end
195
+
196
+ context "active on paypal" do
197
+ before { mock_paypal_profile_details("fgsga564aa", "ActiveProfile", "2010-01-10", "2010-02-10") }
198
+ it("should return next_billing_date") { @sub.next_billing_date.should == @next_billing }
199
+ it("should return last_billing_date") { @sub.last_billing_date.should == Time.parse("2010-01-10 00:00 UTC") }
200
+ it("should return estimated_next_billing_date") { @sub.estimated_next_billing_date.should == @next_billing }
201
+ it("should return successor_billing_start_date") { @sub.successor_billing_start_date.should == @next_billing }
202
+ context "profess successor" do
203
+ before { at_time(@now) { @succ = @sub.subject.build_next_subscription('profess'); @succ.save! } }
204
+ should_build_valid_successor("profess", :now, :next_billing)
205
+ should_have_paid_activation_flow(:succ, false)
206
+ end
207
+ context "basic successor" do
208
+ before { at_time(@now) { @succ = @sub.subject.build_next_subscription('basic'); @succ.save! } }
209
+ should_build_valid_successor("basic", :next_billing, :next_billing)
210
+ should_have_paid_activation_flow(:succ, false)
211
+ end
212
+ context "free successor" do
213
+ before { at_time(@now) { @succ = @sub.subject.build_next_subscription('free'); @succ.save! } }
214
+ should_build_valid_successor("free", :next_billing, :next_billing)
215
+ should_have_free_activation_flow(:succ, false)
216
+ end
217
+ end
218
+
219
+ context "canceled on paypal" do
220
+ before { mock_paypal_profile_details("fgsga564aa", "CanceledProfile", "2010-01-10", nil) }
221
+ it("should return next_billing_date") { @sub.next_billing_date.should be_nil }
222
+ it("should return last_billing_date") { @sub.last_billing_date.should == Time.parse("2010-01-10 00:00 UTC") }
223
+ it("should return estimated_next_billing_date") { @sub.estimated_next_billing_date.should == @next_billing }
224
+ it("should return successor_billing_start_date") { @sub.successor_billing_start_date.should == @next_billing }
225
+ context "profess successor" do
226
+ before { at_time(@now) { @succ = @sub.subject.build_next_subscription('profess'); @succ.save! } }
227
+ should_build_valid_successor("profess", :now, :next_billing)
228
+ end
229
+ context "basic successor" do
230
+ before { at_time(@now) { @succ = @sub.subject.build_next_subscription('basic'); @succ.save! } }
231
+ should_build_valid_successor("basic", :next_billing, :next_billing)
232
+ end
233
+ context "free successor" do
234
+ before { at_time(@now) { @succ = @sub.subject.build_next_subscription('free'); @succ.save! } }
235
+ should_build_valid_successor("free", :next_billing, :next_billing)
236
+ end
237
+ end
238
+
239
+ context "canceled on paypal, no payments made" do
240
+ before { mock_paypal_profile_details("fgsga564aa", "CanceledProfile", nil, nil) }
241
+ it("should return next_billing_date") { @sub.next_billing_date.should be_nil }
242
+ it("should return last_billing_date") { @sub.last_billing_date.should be_nil }
243
+ it("should return estimated_next_billing_date") { @sub.estimated_next_billing_date.should be_nil }
244
+ it("should return successor_billing_start_date") { at_time(@now) { @sub.successor_billing_start_date.should == @now } }
245
+ context "profess successor" do
246
+ before { at_time(@now) { @succ = @sub.subject.build_next_subscription('profess'); @succ.save! } }
247
+ should_build_valid_successor("profess", :now, :now)
248
+ end
249
+ context "basic successor" do
250
+ before { at_time(@now) { @succ = @sub.subject.build_next_subscription('basic'); @succ.save! } }
251
+ should_build_valid_successor("basic", :now, :now)
252
+ end
253
+ context "free successor" do
254
+ before { at_time(@now) { @succ = @sub.subject.build_next_subscription('free'); @succ.save! } }
255
+ should_build_valid_successor("free", :now, :now)
256
+ end
257
+ end
258
+
259
+ context "canceled on our side" do
260
+ before { @sub.update_attributes(:canceled_at => @next_billing, :cancel_reason => 'admin') }
261
+ it("should return successor_billing_start_date") { @sub.successor_billing_start_date.should == @next_billing }
262
+ context "profess successor" do
263
+ before { at_time(@now) { @succ = @sub.subject.build_next_subscription('profess'); @succ.save! } }
264
+ should_build_valid_successor("profess", :now, :next_billing)
265
+ end
266
+ context "basic successor" do
267
+ before { at_time(@now) { @succ = @sub.subject.build_next_subscription('basic'); @succ.save! } }
268
+ should_build_valid_successor("basic", :next_billing, :next_billing)
269
+ end
270
+ context "free successor" do
271
+ before { at_time(@now) { @succ = @sub.subject.build_next_subscription('free'); @succ.save! } }
272
+ should_build_valid_successor("free", :next_billing, :next_billing)
273
+ end
274
+ end
275
+ end
276
+ end
277
+ end
@@ -0,0 +1,135 @@
1
+ require 'spec_helper'
2
+
3
+ describe SubscriptionFu::Transaction do
4
+
5
+ class << self
6
+ def should_have_nogw_initiated_status
7
+ it "should have nogw initiated status" do
8
+ @trans.should_not be_needs_authorization
9
+ @trans.subscription.should_not be_activated
10
+ end
11
+ end
12
+ def should_have_paypal_initiated_status
13
+ it "should have paypal initiated status" do
14
+ @trans.should be_needs_authorization
15
+ @trans.subscription.should_not be_activated
16
+ end
17
+ end
18
+ def should_not_support_start_checkout
19
+ it "should not support start_checkout" do
20
+ lambda { @trans.start_checkout("url1", "url2") }.should raise_error RuntimeError
21
+ end
22
+ end
23
+ def complete_should_transition_to_activated
24
+ context "complete" do
25
+ before { @res = @trans.complete }
26
+ it("should return true") { @res.should == true }
27
+ it "should transition" do
28
+ @trans.status.should == "complete"
29
+ @trans.subscription.should be_activated
30
+ end
31
+ end
32
+ end
33
+ def complete_should_transition_to_canceled
34
+ context "complete" do
35
+ before { @res = @trans.complete }
36
+ it("should return true") { @res.should == true }
37
+ it "should transition" do
38
+ @trans.status.should == "complete"
39
+ @trans.subscription.should be_canceled
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ it { should belong_to :subscription }
46
+ it { should belong_to :initiator }
47
+ it { should belong_to :related_transaction }
48
+ it { should have_many :related_transactions }
49
+
50
+ it { should validate_presence_of :subscription }
51
+ it { should validate_presence_of :initiator }
52
+
53
+ it { should validate_presence_of :gateway }
54
+ %w( paypal nogw ).each {|v| it { should allow_value(v).for(:gateway)} }
55
+ it { should_not allow_value("payPal").for(:gateway) }
56
+
57
+ it { should validate_presence_of :action }
58
+ %w( activation cancellation ).each {|v| it { should allow_value(v).for(:action)} }
59
+ it { should_not allow_value("actiove").for(:action) }
60
+
61
+ it { should validate_presence_of :status }
62
+ %w( initiated complete failed aborted ).each {|v| it { should allow_value(v).for(:status) } }
63
+ it { should_not allow_value("unknown").for(:status) }
64
+
65
+ context "initiated activation nogw transaction" do
66
+ before do
67
+ @sub = Factory(:subscription, :plan_key => 'free')
68
+ @trans = Factory(:transaction, :gateway => "nogw", :status => "initiated", :action => "activation", :subscription => @sub)
69
+ end
70
+ should_have_nogw_initiated_status
71
+
72
+ context "checkout" do
73
+ before do
74
+ @redirect_target = @trans.start_checkout("url1", "url2")
75
+ end
76
+ it "should redirect to confirmation URL" do
77
+ @redirect_target.should == "url1"
78
+ end
79
+ complete_should_transition_to_activated
80
+ end
81
+ end
82
+
83
+ context "initiated cancellation nogw transaction" do
84
+ before do
85
+ @sub = Factory(:subscription, :plan_key => 'free')
86
+ @trans = Factory(:transaction, :gateway => "nogw", :status => "initiated", :action => "cancellation", :subscription => @sub)
87
+ end
88
+ should_have_nogw_initiated_status
89
+ should_not_support_start_checkout
90
+ complete_should_transition_to_canceled
91
+ end
92
+
93
+ context "complete nogw transaction" do
94
+ before { @trans = Factory(:transaction, :gateway => "nogw", :status => "complete") }
95
+ should_not_support_start_checkout
96
+ end
97
+
98
+ context "invalid nogw transaction" do
99
+ before { @trans = Factory(:transaction, :gateway => "nogw", :status => "failed") }
100
+ should_not_support_start_checkout
101
+ end
102
+
103
+ context "initiated activation paypal transaction" do
104
+ before { @trans = Factory(:transaction, :gateway => "paypal", :status => "initiated", :action => "activation") }
105
+ should_have_paypal_initiated_status
106
+ context "checkout" do
107
+ before do
108
+ mock_paypal_express_checkout("bgds65sd")
109
+ mock_paypal_create_profile("bgds65sd")
110
+ @redirect_target = @trans.start_checkout("url1", "url2")
111
+ end
112
+ it "should redirect to paypal" do
113
+ @redirect_target.should == "https://www.paypal.com/cgi-bin/webscr?cmd=_express-checkout&token=bgds65sd"
114
+ @trans.identifier.should == "bgds65sd"
115
+ end
116
+ complete_should_transition_to_activated
117
+ end
118
+ context "complete without checkout" do
119
+ before { @res = @trans.complete }
120
+ it("should return false") { @res.should == false }
121
+ it("should fail") { @trans.status.should == "failed" }
122
+ end
123
+ end
124
+
125
+ context "complete paypal transaction" do
126
+ before { @trans = Factory(:transaction, :gateway => "paypal", :status => "complete") }
127
+ should_not_support_start_checkout
128
+ end
129
+
130
+ context "failed paypal transaction" do
131
+ before { @trans = Factory(:transaction, :gateway => "paypal", :status => "failed") }
132
+ should_not_support_start_checkout
133
+ end
134
+
135
+ end
@@ -0,0 +1,18 @@
1
+ LIB_DIR = File.expand_path('../../', __FILE__)
2
+ $:.unshift File.join(LIB_DIR, 'lib')
3
+
4
+ ENV["RAILS_ENV"] ||= 'test'
5
+ require File.expand_path(File.dirname(__FILE__) + "/app/config/environment")
6
+ require 'rspec/rails'
7
+ require 'shoulda-matchers'
8
+ require 'factory_girl'
9
+ require 'time_travel'
10
+ require 'webmock/rspec'
11
+
12
+ load Rails.root.join('db', 'schema.rb')
13
+
14
+ Factory.definition_file_paths = [ File.join(LIB_DIR, 'spec', 'factories') ]
15
+ Factory.find_definitions
16
+
17
+ Dir[File.join(LIB_DIR, 'spec', 'support', '**', '*.rb')].each {|f| require f}
18
+
@@ -0,0 +1,82 @@
1
+ module PaypalTestHelper
2
+
3
+ class RequestParamMatcher
4
+ def initialize(opts = {})
5
+ @opts = opts
6
+ end
7
+ def =~(request)
8
+ parsed_req = CGI.parse(request.body)
9
+ @opts.all? {|key,value| parsed_req[key].first == value }
10
+ end
11
+ end
12
+
13
+ class PaypalRequestMatcher < RequestParamMatcher
14
+ def initialize(method, opts = {})
15
+ super(opts.merge("METHOD" => method))
16
+ end
17
+ end
18
+
19
+ def mock_paypal_ipn_validation_ok
20
+ stub_request(:post, "https://www.paypal.com/cgi-bin/webscr").
21
+ to_return(:status => 200, :body => "VERIFIED", :headers => {}).
22
+ with{|request| RequestParamMatcher.new("cmd" => "_notify-validate") =~ request }
23
+ end
24
+
25
+ def mock_paypal_ipn_validation_invalid
26
+ stub_request(:post, "https://www.paypal.com/cgi-bin/webscr").
27
+ to_return(:status => 200, :body => "INVALID", :headers => {}).
28
+ with{|request| RequestParamMatcher.new("cmd" => "_notify-validate") =~ request }
29
+ end
30
+
31
+ def mock_paypal_express_checkout(token = "token123")
32
+ stub_request(:post, "https://api-3t.paypal.com/nvp").
33
+ to_return(:status => 200, :body => "ACK=Success&TOKEN=#{token}", :headers => {}).
34
+ with{|request| PaypalRequestMatcher.new("SetExpressCheckout") =~ request }
35
+ end
36
+
37
+ def mock_paypal_profile_details(profile, status, last_billing, next_billing)
38
+ res_details = paypal_profile_res(profile, status, "NEXTBILLINGDATE"=>next_billing, 'LASTPAYMENTDATE'=>last_billing)
39
+ stub_request(:post, "https://api-3t.paypal.com/nvp").
40
+ to_return(:status => 200, :body => res_details, :headers => {}).
41
+ with{|request| PaypalRequestMatcher.new("GetRecurringPaymentsProfileDetails", "PROFILEID" => profile) =~ request }
42
+ end
43
+
44
+ def mock_paypal_create_profile(token, new_profile_id = "49vnq320dsj", status = "ActiveProfile")
45
+ stub_request(:post, "https://api-3t.paypal.com/nvp").
46
+ to_return(:status => 200, :body => paypal_profile_res(new_profile_id, status), :headers => {}).
47
+ with{|request| PaypalRequestMatcher.new("CreateRecurringPaymentsProfile", "TOKEN" => token) =~ request }
48
+ end
49
+
50
+ def mock_paypal_create_profile_with_error(token)
51
+ stub_request(:post, "https://api-3t.paypal.com/nvp").
52
+ to_return(:status => 200, :body => paypal_error_res, :headers => {}).
53
+ with{|request| PaypalRequestMatcher.new("CreateRecurringPaymentsProfile", "TOKEN" => token) =~ request }
54
+ end
55
+
56
+ def mock_paypal_delete_profile(profile_id)
57
+ stub_request(:post, "https://api-3t.paypal.com/nvp").
58
+ to_return(:status => 200, :body => paypal_res("PROFILEID"=>"Cancel"), :headers => {}).
59
+ with{|request| PaypalRequestMatcher.new("ManageRecurringPaymentsProfileStatus", "ACTION"=>"Cancel", "PROFILEID"=>profile_id) =~ request }
60
+ end
61
+
62
+ def mock_paypal_delete_profile_with_error(profile_id)
63
+ stub_request(:post, "https://api-3t.paypal.com/nvp").
64
+ to_return(:status => 200, :body => paypal_error_res, :headers => {}).
65
+ with{|request| PaypalRequestMatcher.new("ManageRecurringPaymentsProfileStatus", "ACTION"=>"Cancel", "PROFILEID"=>profile_id) =~ request }
66
+ end
67
+
68
+ def paypal_res(opts = {})
69
+ opts.reverse_merge("ACK"=>"Success").reject{|k,v| v.nil? }.map{|k,v| "#{CGI.escape(k)}=#{CGI.escape(v)}" }.join('&')
70
+ end
71
+
72
+ def paypal_error_res(opts = {})
73
+ paypal_res(opts.merge("ACK"=>"Failure", "L_SEVERITYCODE0"=>"Error", "L_ERRORCODE0"=>"11556"))
74
+ end
75
+
76
+ def paypal_profile_res(profile, status, opts = {})
77
+ paypal_res(opts.merge("PROFILEID"=>profile,"STATUS"=>status))
78
+ end
79
+
80
+ end
81
+
82
+ RSpec.configuration.include PaypalTestHelper
@@ -0,0 +1,23 @@
1
+ $:.push File.expand_path("../lib", __FILE__)
2
+ require "subscription_fu/version"
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "subscription_fu"
6
+ s.version = SubscriptionFu::VERSION
7
+ s.platform = Gem::Platform::RUBY
8
+ s.authors = ["Paul McMahon", "Michael Reinsch"]
9
+ s.email = "info@mobalean.com"
10
+ s.homepage = "http://www.mobalean.com"
11
+ s.summary = "Rails support for handling free/paid subscriptions"
12
+ s.description = "SubscriptionFu helps with building services which have paid subscriptions. It includes the models to store subscription status, and provides integration with PayPal for paid subscriptions."
13
+
14
+ s.files = `git ls-files`.split("\n")
15
+ s.require_path = 'lib'
16
+ s.rubyforge_project = "subscriptionfu"
17
+
18
+ s.add_dependency 'rails', '>= 3.0.3'
19
+ s.add_dependency 'rest-client', '>= 1.6.1'
20
+
21
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
22
+ end
23
+
metadata ADDED
@@ -0,0 +1,134 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: subscription_fu
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.1.0
6
+ platform: ruby
7
+ authors:
8
+ - Paul McMahon
9
+ - Michael Reinsch
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+
14
+ date: 2011-05-20 00:00:00 +09:00
15
+ default_executable:
16
+ dependencies:
17
+ - !ruby/object:Gem::Dependency
18
+ name: rails
19
+ prerelease: false
20
+ requirement: &id001 !ruby/object:Gem::Requirement
21
+ none: false
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 3.0.3
26
+ type: :runtime
27
+ version_requirements: *id001
28
+ - !ruby/object:Gem::Dependency
29
+ name: rest-client
30
+ prerelease: false
31
+ requirement: &id002 !ruby/object:Gem::Requirement
32
+ none: false
33
+ requirements:
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: 1.6.1
37
+ type: :runtime
38
+ version_requirements: *id002
39
+ description: SubscriptionFu helps with building services which have paid subscriptions. It includes the models to store subscription status, and provides integration with PayPal for paid subscriptions.
40
+ email: info@mobalean.com
41
+ executables: []
42
+
43
+ extensions: []
44
+
45
+ extra_rdoc_files: []
46
+
47
+ files:
48
+ - .gitignore
49
+ - Gemfile
50
+ - LICENSE
51
+ - README.md
52
+ - Rakefile
53
+ - app/models/subscription_fu/plan.rb
54
+ - app/models/subscription_fu/subscription.rb
55
+ - app/models/subscription_fu/transaction.rb
56
+ - config/locales/en.yml
57
+ - examples/routes.rb
58
+ - examples/subscriptions_controller.rb
59
+ - examples/transactions_controller.rb
60
+ - lib/generators/subscription_fu/install_generator.rb
61
+ - lib/generators/subscription_fu/templates/en.yml
62
+ - lib/generators/subscription_fu/templates/initializer.rb
63
+ - lib/generators/subscription_fu/templates/migration.rb
64
+ - lib/subscription_fu.rb
65
+ - lib/subscription_fu/config.rb
66
+ - lib/subscription_fu/engine.rb
67
+ - lib/subscription_fu/models.rb
68
+ - lib/subscription_fu/paypal.rb
69
+ - lib/subscription_fu/railtie.rb
70
+ - lib/subscription_fu/version.rb
71
+ - spec/app/.gitignore
72
+ - spec/app/Rakefile
73
+ - spec/app/app/controllers/application_controller.rb
74
+ - spec/app/app/models/initiator.rb
75
+ - spec/app/app/models/subject.rb
76
+ - spec/app/app/views/layouts/application.html.haml
77
+ - spec/app/config.ru
78
+ - spec/app/config/application.rb
79
+ - spec/app/config/boot.rb
80
+ - spec/app/config/database.yml
81
+ - spec/app/config/environment.rb
82
+ - spec/app/config/environments/development.rb
83
+ - spec/app/config/environments/test.rb
84
+ - spec/app/config/initializers/backtrace_silencers.rb
85
+ - spec/app/config/initializers/inflections.rb
86
+ - spec/app/config/initializers/secret_token.rb
87
+ - spec/app/config/initializers/subscription_fu.rb
88
+ - spec/app/config/locales/subscription_fu.en.yml
89
+ - spec/app/config/routes.rb
90
+ - spec/app/db/.gitignore
91
+ - spec/app/db/migrate/20110516061428_create_subjects.rb
92
+ - spec/app/db/migrate/20110516061443_create_initiators.rb
93
+ - spec/app/db/migrate/20110516070948_create_subscription_fu_tables.rb
94
+ - spec/app/db/schema.rb
95
+ - spec/app/script/rails
96
+ - spec/factories/initiator.rb
97
+ - spec/factories/subject.rb
98
+ - spec/factories/subscription.rb
99
+ - spec/factories/transaction.rb
100
+ - spec/models/subscription_spec.rb
101
+ - spec/models/subscription_transaction_spec.rb
102
+ - spec/spec_helper.rb
103
+ - spec/support/paypal_test_helper.rb
104
+ - subscription_fu.gemspec
105
+ has_rdoc: true
106
+ homepage: http://www.mobalean.com
107
+ licenses: []
108
+
109
+ post_install_message:
110
+ rdoc_options: []
111
+
112
+ require_paths:
113
+ - lib
114
+ required_ruby_version: !ruby/object:Gem::Requirement
115
+ none: false
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: "0"
120
+ required_rubygems_version: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: "0"
126
+ requirements: []
127
+
128
+ rubyforge_project: subscriptionfu
129
+ rubygems_version: 1.6.2
130
+ signing_key:
131
+ specification_version: 3
132
+ summary: Rails support for handling free/paid subscriptions
133
+ test_files: []
134
+