subscription_fu 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -6,7 +6,6 @@ gemspec
6
6
  group :development, :test do
7
7
  gem 'rails'
8
8
  gem 'sqlite3'
9
- gem 'rake', "0.8.7"
10
9
  gem 'haml'
11
10
  gem 'rspec', '>= 2.5.0'
12
11
  gem 'rspec-rails'
data/README.md CHANGED
@@ -22,6 +22,10 @@ Then install the required files:
22
22
 
23
23
  rails g subscription_fu:install
24
24
 
25
+ ## Updating
26
+
27
+ See UPDATING.md
28
+
25
29
  ## Configuration
26
30
 
27
31
  1. Edit config/initializers/subscription\_fu.rb (generated by the install generator)
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
- # TODO this should probably only take plan?key, prev_sub
24
- def self.build_for_initializing(plan_key, start_time = Time.now, billing_start_time = start_time, prev_sub = nil)
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
- successor_billing_start_date
90
+ end_date_when_canceled
78
91
  end
79
92
  end
80
93
 
81
- def successor_billing_start_date
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(admin)
102
+ def initiate_activation(initiator)
90
103
  gateway = (plan.free_plan? || sponsored?) ? 'nogw' : 'paypal'
91
- transactions.create_activation(gateway, admin).tap do |t|
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(admin, t) }
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(admin, activation_transaction)
101
- transactions.create_cancellation(admin, activation_transaction, self)
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 = (err.respond_to?(:data) ? err.data : {}).merge(:subscription => subscription.inspect, :transaction => self.inspect)
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
- # update the record beforehand, because paypal raises an error if
129
- # the profile is already cancelled
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
- if active_subscription
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
- { :next_billing_date => UTC_TZ.parse(res.recurring.summary.next_billing_date.to_s),
17
- :last_payment_date => UTC_TZ.parse(res.recurring.summary.last_payment_date.to_s), }
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
@@ -6,6 +6,9 @@ module SubscriptionFu
6
6
  include SubscriptionFu::Models
7
7
  end
8
8
  end
9
+ rake_tasks do
10
+ load "subscription_fu/rake.tasks"
11
+ end
9
12
  end
10
13
  end
11
14
 
@@ -0,0 +1,6 @@
1
+ namespace :subfu do
2
+ desc "sync subscriptions with gateways"
3
+ task :gwsync => :environment do
4
+ SubscriptionFu::Subscription.sync_all_from_gateway
5
+ end
6
+ end
@@ -1,3 +1,3 @@
1
1
  module SubscriptionFu
2
- VERSION = "0.2.1"
2
+ VERSION = "0.3.0"
3
3
  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
@@ -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 => 20110516070948) do
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 == "failed"
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", "ActiveProfile", "2010-01-10", "2010-02-10") }
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 successor_billing_start_date") { @sub.successor_billing_start_date.should == @next_billing }
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", "CanceledProfile", "2010-01-10", nil) }
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 successor_billing_start_date") { @sub.successor_billing_start_date.should == @next_billing }
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", "CanceledProfile", nil, nil) }
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 successor_billing_start_date") { at_time(@now) { @sub.successor_billing_start_date.should == @now } }
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 successor_billing_start_date") { @sub.successor_billing_start_date.should == @next_billing }
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: false
5
- segments:
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-06-07 00:00:00 +09:00
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.3.7
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