subscription_fu 0.1.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.
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
+