subscription_fu 0.2.1 → 0.3.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/Gemfile +0 -1
- data/README.md +4 -0
- data/UPDATING.md +19 -0
- data/app/models/subscription_fu/subscription.rb +32 -10
- data/app/models/subscription_fu/system_initiator.rb +7 -0
- data/app/models/subscription_fu/transaction.rb +14 -4
- data/lib/generators/subscription_fu/templates/migration.rb +6 -0
- data/lib/subscription_fu/models.rb +1 -6
- data/lib/subscription_fu/paypal.rb +6 -2
- data/lib/subscription_fu/railtie.rb +3 -0
- data/lib/subscription_fu/rake.tasks +6 -0
- data/lib/subscription_fu/version.rb +1 -1
- data/spec/app/db/migrate/20110701061927_subscription_fu_update_to_zero_three.rb +12 -0
- data/spec/app/db/schema.rb +6 -1
- data/spec/models/subscription_spec.rb +18 -8
- metadata +8 -20
data/Gemfile
CHANGED
data/README.md
CHANGED
data/UPDATING.md
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# Updating Subscriptions for Rails
|
2
|
+
|
3
|
+
## From 0.2.x to 0.3.0
|
4
|
+
|
5
|
+
Add the new table subscription_system_initiators using a migration:
|
6
|
+
|
7
|
+
class SubscriptionFuUpdateToZeroThree < ActiveRecord::Migration
|
8
|
+
def self.up
|
9
|
+
create_table "subscription_system_initiators" do |t|
|
10
|
+
t.string "name"
|
11
|
+
t.string "description"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
def self.down
|
15
|
+
drop_table "subscription_system_initiators"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
|
@@ -1,7 +1,7 @@
|
|
1
1
|
class SubscriptionFu::Subscription < ActiveRecord::Base
|
2
2
|
set_table_name :subscriptions
|
3
3
|
|
4
|
-
AVAILABLE_CANCEL_REASONS = %w( update cancel timeout admin )
|
4
|
+
AVAILABLE_CANCEL_REASONS = %w( update cancel gwcancel timeout admin )
|
5
5
|
|
6
6
|
default_scope order("created_at ASC", "id ASC")
|
7
7
|
|
@@ -18,10 +18,23 @@ class SubscriptionFu::Subscription < ActiveRecord::Base
|
|
18
18
|
validates :cancel_reason, :presence => true, :inclusion => AVAILABLE_CANCEL_REASONS, :if => :canceled?
|
19
19
|
|
20
20
|
scope :activated, where("subscriptions.activated_at IS NOT NULL")
|
21
|
+
scope :not_canceled, activated.where("subscriptions.canceled_at IS NULL")
|
22
|
+
scope :using_paypal, where("subscriptions.paypal_profile_id IS NOT NULL")
|
21
23
|
scope :current, lambda {|time| activated.where("subscriptions.starts_at <= ? AND (subscriptions.canceled_at IS NULL OR subscriptions.canceled_at > ?)", time, time) }
|
22
24
|
|
23
|
-
|
24
|
-
|
25
|
+
def self.sync_all_from_gateway
|
26
|
+
SubscriptionFu::Subscription.using_paypal.not_canceled.each do |s|
|
27
|
+
s.sync_from_gateway!
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.build_for_initializing(plan_key, prev_sub = nil)
|
32
|
+
if prev_sub
|
33
|
+
start_time = prev_sub.successor_start_date(plan_key)
|
34
|
+
billing_start_time = prev_sub.end_date_when_canceled
|
35
|
+
else
|
36
|
+
billing_start_time = start_time = Time.now
|
37
|
+
end
|
25
38
|
new(:plan_key => plan_key, :starts_at => start_time, :billing_starts_at => billing_start_time, :prev_subscription => prev_sub)
|
26
39
|
end
|
27
40
|
|
@@ -74,11 +87,11 @@ class SubscriptionFu::Subscription < ActiveRecord::Base
|
|
74
87
|
Time.now
|
75
88
|
else
|
76
89
|
# otherwise they start with the next billing cycle
|
77
|
-
|
90
|
+
end_date_when_canceled
|
78
91
|
end
|
79
92
|
end
|
80
93
|
|
81
|
-
def
|
94
|
+
def end_date_when_canceled
|
82
95
|
# in case this plan was already canceled, this date takes
|
83
96
|
# precedence (there won't be a next billing time anymore).
|
84
97
|
canceled_at || next_billing_date || estimated_next_billing_date || Time.now
|
@@ -86,19 +99,28 @@ class SubscriptionFu::Subscription < ActiveRecord::Base
|
|
86
99
|
|
87
100
|
# billing API
|
88
101
|
|
89
|
-
def initiate_activation(
|
102
|
+
def initiate_activation(initiator)
|
90
103
|
gateway = (plan.free_plan? || sponsored?) ? 'nogw' : 'paypal'
|
91
|
-
transactions.create_activation(gateway,
|
104
|
+
transactions.create_activation(gateway, initiator).tap do |t|
|
92
105
|
if prev_subscription
|
93
106
|
to_cancel = [prev_subscription]
|
94
107
|
to_cancel.push(*prev_subscription.next_subscriptions.where("subscriptions.id <> ?", self).all)
|
95
|
-
to_cancel.each {|s| s.initiate_cancellation(
|
108
|
+
to_cancel.each {|s| s.initiate_cancellation(initiator, t) }
|
96
109
|
end
|
97
110
|
end
|
98
111
|
end
|
99
112
|
|
100
|
-
def initiate_cancellation(
|
101
|
-
transactions.create_cancellation(
|
113
|
+
def initiate_cancellation(initiator, activation_transaction)
|
114
|
+
transactions.create_cancellation(initiator, activation_transaction, self)
|
115
|
+
end
|
116
|
+
|
117
|
+
def sync_from_gateway!
|
118
|
+
if paypal?
|
119
|
+
if paypal_recurring_details[:status] == SubscriptionFu::Paypal::CANCELED_STATE
|
120
|
+
t = initiate_cancellation(SubscriptionFu::SystemInitiator.paypal_sync_initiator, nil)
|
121
|
+
t.complete(:effective => end_date_when_canceled, :reason => :gwcancel)
|
122
|
+
end
|
123
|
+
end
|
102
124
|
end
|
103
125
|
|
104
126
|
private
|
@@ -0,0 +1,7 @@
|
|
1
|
+
class SubscriptionFu::SystemInitiator < ActiveRecord::Base
|
2
|
+
set_table_name :subscription_system_initiators
|
3
|
+
|
4
|
+
def self.paypal_sync_initiator
|
5
|
+
find_or_create_by_name("paypal sync", :description => "Updates subscription status based on status returned by Paypal")
|
6
|
+
end
|
7
|
+
end
|
@@ -54,8 +54,11 @@ class SubscriptionFu::Transaction < ActiveRecord::Base
|
|
54
54
|
update_attributes!(:status => "complete")
|
55
55
|
rescue Exception => err
|
56
56
|
if defined? ::ExceptionNotifier
|
57
|
-
data =
|
57
|
+
data = {:api_response => err.respond_to?(:response) ? err.response : nil, :subscription => subscription.inspect, :transaction => self.inspect}
|
58
58
|
::ExceptionNotifier::Notifier.background_exception_notification(err, :data => data).deliver
|
59
|
+
elsif defined? ::HoptoadNotifier
|
60
|
+
data = {:subscription => subscription.inspect, :transaction => self.inspect}
|
61
|
+
::HoptoadNotifier.notify(err, :parameters => data)
|
59
62
|
else
|
60
63
|
logger.warn(err)
|
61
64
|
logger.debug(err.backtrace.join("\n"))
|
@@ -125,10 +128,17 @@ class SubscriptionFu::Transaction < ActiveRecord::Base
|
|
125
128
|
end
|
126
129
|
|
127
130
|
def complete_cancellation_paypal(opts)
|
128
|
-
|
129
|
-
|
131
|
+
begin
|
132
|
+
SubscriptionFu::Paypal.express_request.renew!(sub_paypal_profile_id, :Cancel, :note => sub_cancel_reason)
|
133
|
+
rescue Paypal::Exception::APIError => err
|
134
|
+
if err.response.details.all?{|d| d.error_code == "11556"}
|
135
|
+
# 11556 - Invalid profile status for cancel action; profile should be active or suspended
|
136
|
+
logger.info("Got '#{err.response.details.inspect}' from paypal which indicates profile wasn't active (any more)...")
|
137
|
+
else
|
138
|
+
raise err
|
139
|
+
end
|
140
|
+
end
|
130
141
|
complete_cancellation(opts)
|
131
|
-
SubscriptionFu::Paypal.express_request.renew!(sub_paypal_profile_id, :Cancel, :note => sub_cancel_reason)
|
132
142
|
end
|
133
143
|
|
134
144
|
def complete_cancellation_nogw(opts)
|
@@ -29,10 +29,16 @@ class CreateSubscriptionFuTables < ActiveRecord::Migration
|
|
29
29
|
|
30
30
|
add_index "subscription_transactions", ["identifier"]
|
31
31
|
add_index "subscription_transactions", ["subscription_id"]
|
32
|
+
|
33
|
+
create_table "subscription_system_initiators" do |t|
|
34
|
+
t.string "name"
|
35
|
+
t.string "description"
|
36
|
+
end
|
32
37
|
end
|
33
38
|
|
34
39
|
def self.down
|
35
40
|
drop_table "subscriptions"
|
36
41
|
drop_table "subscription_transactions"
|
42
|
+
drop_table "subscription_system_initiators"
|
37
43
|
end
|
38
44
|
end
|
@@ -41,12 +41,7 @@ module SubscriptionFu
|
|
41
41
|
end
|
42
42
|
|
43
43
|
def build_next_subscription(plan_key)
|
44
|
-
|
45
|
-
# TODO refactor
|
46
|
-
subscriptions.build_for_initializing(plan_key, active_subscription.successor_start_date(plan_key), active_subscription.successor_billing_start_date, active_subscription)
|
47
|
-
else
|
48
|
-
subscriptions.build_for_initializing(plan_key)
|
49
|
-
end
|
44
|
+
subscriptions.build_for_initializing(plan_key, active_subscription)
|
50
45
|
end
|
51
46
|
end
|
52
47
|
|
@@ -3,6 +3,9 @@ require "paypal"
|
|
3
3
|
module SubscriptionFu::Paypal
|
4
4
|
UTC_TZ = ActiveSupport::TimeZone.new("UTC")
|
5
5
|
|
6
|
+
CANCELED_STATE = "Cancelled"
|
7
|
+
ACTIVE_STATE = "Active"
|
8
|
+
|
6
9
|
def self.express_request
|
7
10
|
config = SubscriptionFu.config
|
8
11
|
::Paypal::Express::Request.new(
|
@@ -13,7 +16,8 @@ module SubscriptionFu::Paypal
|
|
13
16
|
|
14
17
|
def self.recurring_details(profile_id)
|
15
18
|
res = SubscriptionFu::Paypal.express_request.subscription(profile_id)
|
16
|
-
{ :
|
17
|
-
:
|
19
|
+
{ :status => res.recurring.status,
|
20
|
+
:next_billing_date => UTC_TZ.parse(res.recurring.summary.next_billing_date.to_s),
|
21
|
+
:last_payment_date => UTC_TZ.parse(res.recurring.summary.last_payment_date.to_s) }
|
18
22
|
end
|
19
23
|
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class SubscriptionFuUpdateToZeroThree < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table "subscription_system_initiators" do |t|
|
4
|
+
t.string "name"
|
5
|
+
t.string "description"
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.down
|
10
|
+
drop_table "subscription_system_initiators"
|
11
|
+
end
|
12
|
+
end
|
data/spec/app/db/schema.rb
CHANGED
@@ -10,7 +10,7 @@
|
|
10
10
|
#
|
11
11
|
# It's strongly recommended to check this file into your version control system.
|
12
12
|
|
13
|
-
ActiveRecord::Schema.define(:version =>
|
13
|
+
ActiveRecord::Schema.define(:version => 20110701061927) do
|
14
14
|
|
15
15
|
create_table "initiators", :force => true do |t|
|
16
16
|
t.string "desc"
|
@@ -25,6 +25,11 @@ ActiveRecord::Schema.define(:version => 20110516070948) do
|
|
25
25
|
t.datetime "updated_at"
|
26
26
|
end
|
27
27
|
|
28
|
+
create_table "subscription_system_initiators", :force => true do |t|
|
29
|
+
t.string "name"
|
30
|
+
t.string "description"
|
31
|
+
end
|
32
|
+
|
28
33
|
create_table "subscription_transactions", :force => true do |t|
|
29
34
|
t.integer "subscription_id", :null => false
|
30
35
|
t.integer "initiator_id", :null => false
|
@@ -117,7 +117,7 @@ describe SubscriptionFu::Subscription do
|
|
117
117
|
it "should cancel previous sub with failure" do
|
118
118
|
sub = instance_variable_get("@#{sub_instance}").prev_subscription.reload
|
119
119
|
sub.canceled_at.should be_present
|
120
|
-
sub.transactions.last.status.should == "
|
120
|
+
sub.transactions.last.status.should == "complete"
|
121
121
|
end
|
122
122
|
end unless first_sub || prev_sub_is_free
|
123
123
|
context "complete with error in create" do
|
@@ -194,11 +194,11 @@ describe SubscriptionFu::Subscription do
|
|
194
194
|
end
|
195
195
|
|
196
196
|
context "active on paypal" do
|
197
|
-
before { mock_paypal_profile_details("fgsga564aa", "
|
197
|
+
before { mock_paypal_profile_details("fgsga564aa", "Active", "2010-01-10", "2010-02-10") }
|
198
198
|
it("should return next_billing_date") { @sub.next_billing_date.should == @next_billing }
|
199
199
|
it("should return last_billing_date") { @sub.last_billing_date.should == Time.parse("2010-01-10 00:00 UTC") }
|
200
200
|
it("should return estimated_next_billing_date") { @sub.estimated_next_billing_date.should == @next_billing }
|
201
|
-
it("should return
|
201
|
+
it("should return end_date_when_canceled") { @sub.end_date_when_canceled.should == @next_billing }
|
202
202
|
context "profess successor" do
|
203
203
|
before { at_time(@now) { @succ = @sub.subject.build_next_subscription('profess'); @succ.save! } }
|
204
204
|
should_build_valid_successor("profess", :now, :next_billing)
|
@@ -217,11 +217,11 @@ describe SubscriptionFu::Subscription do
|
|
217
217
|
end
|
218
218
|
|
219
219
|
context "canceled on paypal" do
|
220
|
-
before { mock_paypal_profile_details("fgsga564aa", "
|
220
|
+
before { mock_paypal_profile_details("fgsga564aa", "Cancelled", "2010-01-10", nil) }
|
221
221
|
it("should return next_billing_date") { @sub.next_billing_date.should be_nil }
|
222
222
|
it("should return last_billing_date") { @sub.last_billing_date.should == Time.parse("2010-01-10 00:00 UTC") }
|
223
223
|
it("should return estimated_next_billing_date") { @sub.estimated_next_billing_date.should == @next_billing }
|
224
|
-
it("should return
|
224
|
+
it("should return end_date_when_canceled") { @sub.end_date_when_canceled.should == @next_billing }
|
225
225
|
context "profess successor" do
|
226
226
|
before { at_time(@now) { @succ = @sub.subject.build_next_subscription('profess'); @succ.save! } }
|
227
227
|
should_build_valid_successor("profess", :now, :next_billing)
|
@@ -234,14 +234,24 @@ describe SubscriptionFu::Subscription do
|
|
234
234
|
before { at_time(@now) { @succ = @sub.subject.build_next_subscription('free'); @succ.save! } }
|
235
235
|
should_build_valid_successor("free", :next_billing, :next_billing)
|
236
236
|
end
|
237
|
+
context "sync" do
|
238
|
+
before { mock_paypal_delete_profile_with_error("fgsga564aa") }
|
239
|
+
before { at_time(@now) { @sub.sync_from_gateway! } }
|
240
|
+
it "should mark subscription as cancelled" do
|
241
|
+
@sub.reload.should be_canceled
|
242
|
+
@sub.canceled_at.should == Time.parse("2010-02-10 00:00 UTC")
|
243
|
+
@sub.cacnel_reason.should == "gwcacnel"
|
244
|
+
@sub.transactions.last.status.should == "complete"
|
245
|
+
end
|
246
|
+
end
|
237
247
|
end
|
238
248
|
|
239
249
|
context "canceled on paypal, no payments made" do
|
240
|
-
before { mock_paypal_profile_details("fgsga564aa", "
|
250
|
+
before { mock_paypal_profile_details("fgsga564aa", "Cancelled", nil, nil) }
|
241
251
|
it("should return next_billing_date") { @sub.next_billing_date.should be_nil }
|
242
252
|
it("should return last_billing_date") { @sub.last_billing_date.should be_nil }
|
243
253
|
it("should return estimated_next_billing_date") { @sub.estimated_next_billing_date.should be_nil }
|
244
|
-
it("should return
|
254
|
+
it("should return end_date_when_canceled") { at_time(@now) { @sub.end_date_when_canceled.should == @now } }
|
245
255
|
context "profess successor" do
|
246
256
|
before { at_time(@now) { @succ = @sub.subject.build_next_subscription('profess'); @succ.save! } }
|
247
257
|
should_build_valid_successor("profess", :now, :now)
|
@@ -258,7 +268,7 @@ describe SubscriptionFu::Subscription do
|
|
258
268
|
|
259
269
|
context "canceled on our side" do
|
260
270
|
before { @sub.update_attributes(:canceled_at => @next_billing, :cancel_reason => 'admin') }
|
261
|
-
it("should return
|
271
|
+
it("should return end_date_when_canceled") { @sub.end_date_when_canceled.should == @next_billing }
|
262
272
|
context "profess successor" do
|
263
273
|
before { at_time(@now) { @succ = @sub.subject.build_next_subscription('profess'); @succ.save! } }
|
264
274
|
should_build_valid_successor("profess", :now, :next_billing)
|
metadata
CHANGED
@@ -1,12 +1,8 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: subscription_fu
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
prerelease:
|
5
|
-
|
6
|
-
- 0
|
7
|
-
- 2
|
8
|
-
- 1
|
9
|
-
version: 0.2.1
|
4
|
+
prerelease:
|
5
|
+
version: 0.3.0
|
10
6
|
platform: ruby
|
11
7
|
authors:
|
12
8
|
- Paul McMahon
|
@@ -15,7 +11,7 @@ autorequire:
|
|
15
11
|
bindir: bin
|
16
12
|
cert_chain: []
|
17
13
|
|
18
|
-
date: 2011-
|
14
|
+
date: 2011-07-01 00:00:00 +09:00
|
19
15
|
default_executable:
|
20
16
|
dependencies:
|
21
17
|
- !ruby/object:Gem::Dependency
|
@@ -26,10 +22,6 @@ dependencies:
|
|
26
22
|
requirements:
|
27
23
|
- - ">="
|
28
24
|
- !ruby/object:Gem::Version
|
29
|
-
segments:
|
30
|
-
- 3
|
31
|
-
- 0
|
32
|
-
- 3
|
33
25
|
version: 3.0.3
|
34
26
|
type: :runtime
|
35
27
|
version_requirements: *id001
|
@@ -41,10 +33,6 @@ dependencies:
|
|
41
33
|
requirements:
|
42
34
|
- - ~>
|
43
35
|
- !ruby/object:Gem::Version
|
44
|
-
segments:
|
45
|
-
- 0
|
46
|
-
- 3
|
47
|
-
- 0
|
48
36
|
version: 0.3.0
|
49
37
|
type: :runtime
|
50
38
|
version_requirements: *id002
|
@@ -62,8 +50,10 @@ files:
|
|
62
50
|
- LICENSE
|
63
51
|
- README.md
|
64
52
|
- Rakefile
|
53
|
+
- UPDATING.md
|
65
54
|
- app/models/subscription_fu/plan.rb
|
66
55
|
- app/models/subscription_fu/subscription.rb
|
56
|
+
- app/models/subscription_fu/system_initiator.rb
|
67
57
|
- app/models/subscription_fu/transaction.rb
|
68
58
|
- config/locales/en.yml
|
69
59
|
- examples/routes.rb
|
@@ -79,6 +69,7 @@ files:
|
|
79
69
|
- lib/subscription_fu/models.rb
|
80
70
|
- lib/subscription_fu/paypal.rb
|
81
71
|
- lib/subscription_fu/railtie.rb
|
72
|
+
- lib/subscription_fu/rake.tasks
|
82
73
|
- lib/subscription_fu/version.rb
|
83
74
|
- spec/app/.gitignore
|
84
75
|
- spec/app/Rakefile
|
@@ -103,6 +94,7 @@ files:
|
|
103
94
|
- spec/app/db/migrate/20110516061428_create_subjects.rb
|
104
95
|
- spec/app/db/migrate/20110516061443_create_initiators.rb
|
105
96
|
- spec/app/db/migrate/20110516070948_create_subscription_fu_tables.rb
|
97
|
+
- spec/app/db/migrate/20110701061927_subscription_fu_update_to_zero_three.rb
|
106
98
|
- spec/app/db/schema.rb
|
107
99
|
- spec/app/script/rails
|
108
100
|
- spec/factories/initiator.rb
|
@@ -129,21 +121,17 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
129
121
|
requirements:
|
130
122
|
- - ">="
|
131
123
|
- !ruby/object:Gem::Version
|
132
|
-
segments:
|
133
|
-
- 0
|
134
124
|
version: "0"
|
135
125
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
136
126
|
none: false
|
137
127
|
requirements:
|
138
128
|
- - ">="
|
139
129
|
- !ruby/object:Gem::Version
|
140
|
-
segments:
|
141
|
-
- 0
|
142
130
|
version: "0"
|
143
131
|
requirements: []
|
144
132
|
|
145
133
|
rubyforge_project: subscriptionfu
|
146
|
-
rubygems_version: 1.
|
134
|
+
rubygems_version: 1.6.2
|
147
135
|
signing_key:
|
148
136
|
specification_version: 3
|
149
137
|
summary: Rails support for handling free/paid subscriptions
|