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