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