subscription_fu 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +16 -0
- data/LICENSE +20 -0
- data/README.md +96 -0
- data/Rakefile +6 -0
- data/app/models/subscription_fu/plan.rb +43 -0
- data/app/models/subscription_fu/subscription.rb +117 -0
- data/app/models/subscription_fu/transaction.rb +131 -0
- data/config/locales/en.yml +7 -0
- data/examples/routes.rb +7 -0
- data/examples/subscriptions_controller.rb +12 -0
- data/examples/transactions_controller.rb +33 -0
- data/lib/generators/subscription_fu/install_generator.rb +23 -0
- data/lib/generators/subscription_fu/templates/en.yml +17 -0
- data/lib/generators/subscription_fu/templates/initializer.rb +24 -0
- data/lib/generators/subscription_fu/templates/migration.rb +38 -0
- data/lib/subscription_fu/config.rb +36 -0
- data/lib/subscription_fu/engine.rb +5 -0
- data/lib/subscription_fu/models.rb +54 -0
- data/lib/subscription_fu/paypal.rb +101 -0
- data/lib/subscription_fu/railtie.rb +12 -0
- data/lib/subscription_fu/version.rb +3 -0
- data/lib/subscription_fu.rb +15 -0
- data/spec/app/.gitignore +1 -0
- data/spec/app/Rakefile +8 -0
- data/spec/app/app/controllers/application_controller.rb +6 -0
- data/spec/app/app/models/initiator.rb +2 -0
- data/spec/app/app/models/subject.rb +4 -0
- data/spec/app/app/views/layouts/application.html.haml +6 -0
- data/spec/app/config/application.rb +11 -0
- data/spec/app/config/boot.rb +4 -0
- data/spec/app/config/database.yml +18 -0
- data/spec/app/config/environment.rb +5 -0
- data/spec/app/config/environments/development.rb +18 -0
- data/spec/app/config/environments/test.rb +22 -0
- data/spec/app/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/app/config/initializers/inflections.rb +2 -0
- data/spec/app/config/initializers/secret_token.rb +2 -0
- data/spec/app/config/initializers/subscription_fu.rb +28 -0
- data/spec/app/config/locales/subscription_fu.en.yml +17 -0
- data/spec/app/config/routes.rb +3 -0
- data/spec/app/config.ru +4 -0
- data/spec/app/db/.gitignore +1 -0
- data/spec/app/db/migrate/20110516061428_create_subjects.rb +13 -0
- data/spec/app/db/migrate/20110516061443_create_initiators.rb +14 -0
- data/spec/app/db/migrate/20110516070948_create_subscription_fu_tables.rb +38 -0
- data/spec/app/db/schema.rb +62 -0
- data/spec/app/script/rails +10 -0
- data/spec/factories/initiator.rb +4 -0
- data/spec/factories/subject.rb +2 -0
- data/spec/factories/subscription.rb +6 -0
- data/spec/factories/transaction.rb +7 -0
- data/spec/models/subscription_spec.rb +277 -0
- data/spec/models/subscription_transaction_spec.rb +135 -0
- data/spec/spec_helper.rb +18 -0
- data/spec/support/paypal_test_helper.rb +82 -0
- data/subscription_fu.gemspec +23 -0
- 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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|
+
|