spree_account_recurring 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +26 -0
- data/README.md +108 -0
- data/app/assets/javascripts/admin/spree_account_recurring.js +1 -0
- data/app/assets/javascripts/store/spree_account_recurring.js +1 -0
- data/app/assets/javascripts/store/stripe.js +53 -0
- data/app/assets/stylesheets/admin/spree_account_recurring.css +3 -0
- data/app/assets/stylesheets/store/spree_account_recurring.css +3 -0
- data/app/controllers/concerns/spree/admin/ransack_date_search.rb +47 -0
- data/app/controllers/spree/admin/plans_controller.rb +63 -0
- data/app/controllers/spree/admin/recurrings_controller.rb +66 -0
- data/app/controllers/spree/admin/subscription_events_controller.rb +14 -0
- data/app/controllers/spree/admin/subscriptions_controller.rb +14 -0
- data/app/controllers/spree/plans_controller.rb +7 -0
- data/app/controllers/spree/recurring_hooks_controller.rb +53 -0
- data/app/controllers/spree/subscriptions_controller.rb +69 -0
- data/app/models/concerns/before_each.rb +21 -0
- data/app/models/concerns/restrictive_destroyer.rb +44 -0
- data/app/models/concerns/spree/plan/api_handler.rb +46 -0
- data/app/models/concerns/spree/recurring/stripe_recurring/api_handler.rb +28 -0
- data/app/models/concerns/spree/recurring/stripe_recurring/api_handler/plan_api_handler.rb +49 -0
- data/app/models/concerns/spree/recurring/stripe_recurring/api_handler/subscription_api_handler.rb +20 -0
- data/app/models/concerns/spree/recurring/stripe_recurring/api_handler/subscription_event_api_handler.rb +19 -0
- data/app/models/concerns/spree/subscription/api_handler.rb +38 -0
- data/app/models/concerns/spree/subscription/role_subscriber.rb +36 -0
- data/app/models/spree/plan.rb +31 -0
- data/app/models/spree/recurring.rb +27 -0
- data/app/models/spree/recurring/stripe_recurring.rb +16 -0
- data/app/models/spree/subscriber_ability.rb +17 -0
- data/app/models/spree/subscription.rb +33 -0
- data/app/models/spree/subscription_event.rb +11 -0
- data/app/models/spree/user_decorator.rb +14 -0
- data/app/overrides/admin/view_decorator.rb +27 -0
- data/app/views/spree/admin/plans/_form.html.erb +47 -0
- data/app/views/spree/admin/plans/destroy.js.erb +0 -0
- data/app/views/spree/admin/plans/edit.html.erb +22 -0
- data/app/views/spree/admin/plans/index.html.erb +70 -0
- data/app/views/spree/admin/plans/new.html.erb +22 -0
- data/app/views/spree/admin/recurrings/_form.html.erb +42 -0
- data/app/views/spree/admin/recurrings/destroy.js.erb +0 -0
- data/app/views/spree/admin/recurrings/edit.html.erb +23 -0
- data/app/views/spree/admin/recurrings/index.html.erb +48 -0
- data/app/views/spree/admin/recurrings/new.html.erb +22 -0
- data/app/views/spree/admin/subscription_events/index.html.erb +72 -0
- data/app/views/spree/admin/subscriptions/index.html.erb +76 -0
- data/app/views/spree/plans/index.html.erb +14 -0
- data/app/views/spree/subscriptions/new.html.erb +36 -0
- data/app/views/spree/subscriptions/show.html.erb +4 -0
- data/config/initializers/stripe.rb +2 -0
- data/config/locales/en.yml +43 -0
- data/config/routes.rb +24 -0
- data/db/migrate/20131119054112_create_spree_recurring.rb +13 -0
- data/db/migrate/20131119054212_create_spree_plan.rb +21 -0
- data/db/migrate/20131119054322_create_spree_subscription.rb +16 -0
- data/db/migrate/20131125123820_create_spree_subscription_events.rb +14 -0
- data/db/migrate/20131202110012_add_subscriber_role_to_spree_roles.rb +15 -0
- data/db/migrate/20140301141327_add_stripe_customer_id_to_spree_users.rb +6 -0
- data/db/migrate/20140303072857_add_default_to_spree_plans.rb +6 -0
- data/db/migrate/20140319092254_add_response_to_spree_subscription_events.rb +5 -0
- data/lib/generators/spree_account_recurring/install/install_generator.rb +31 -0
- data/lib/spree_account_recurring.rb +2 -0
- data/lib/spree_account_recurring/engine.rb +22 -0
- data/lib/spree_account_recurring/factories.rb +6 -0
- data/lib/spree_account_recurring/testing_support/ransack_date_search_helper.rb +125 -0
- metadata +188 -0
@@ -0,0 +1,69 @@
|
|
1
|
+
module Spree
|
2
|
+
class SubscriptionsController < StoreController
|
3
|
+
prepend_before_filter :load_object
|
4
|
+
before_action :find_active_plan, only: [:new, :create]
|
5
|
+
before_action :find_plan, only: [:show, :destroy]
|
6
|
+
before_action :find_subscription, only: [:show, :destroy]
|
7
|
+
before_action :authenticate_subscription, only: [:new, :create]
|
8
|
+
|
9
|
+
def new
|
10
|
+
@subscription = @plan.subscriptions.build
|
11
|
+
end
|
12
|
+
|
13
|
+
def create
|
14
|
+
@subscription = @plan.subscriptions.build(subscription_params.merge(user_id: spree_current_user.id))
|
15
|
+
if @subscription.save_and_manage_api
|
16
|
+
redirect_to recurring_plan_subscription_url(@plan, @subscription), notice: "Thank you for subscribing!"
|
17
|
+
else
|
18
|
+
render :new
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def destroy
|
23
|
+
if @subscription.save_and_manage_api(unsubscribed_at: Time.current)
|
24
|
+
redirect_to plans_path, notice: "Subscription has been cancelled."
|
25
|
+
else
|
26
|
+
render :show
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def find_active_plan
|
33
|
+
unless @plan = Spree::Plan.active.where(id: params[:plan_id]).first
|
34
|
+
flash[:error] = "Plan not found."
|
35
|
+
redirect_to plans_url
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def find_plan
|
40
|
+
unless @plan = Spree::Plan.where(id: params[:plan_id]).first
|
41
|
+
flash[:error] = "Plan not found."
|
42
|
+
redirect_to plans_url
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def find_subscription
|
47
|
+
unless @subscription = @plan.subscriptions.undeleted.where(id: params[:id]).first
|
48
|
+
flash[:error] = "Subscription not found."
|
49
|
+
redirect_to root_url
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def subscription_params
|
54
|
+
params.require(:subscription).permit(:email, :card_token)
|
55
|
+
end
|
56
|
+
|
57
|
+
def load_object
|
58
|
+
@user ||= spree_current_user
|
59
|
+
authorize! params[:action].to_sym, @user
|
60
|
+
end
|
61
|
+
|
62
|
+
def authenticate_subscription
|
63
|
+
if subscription = spree_current_user.subscriptions.undeleted.first
|
64
|
+
flash[:alert] = "You have already subscribed."
|
65
|
+
redirect_to recurring_plan_subscription_url(@plan, subscription)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module BeforeEach
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
module ClassMethods
|
5
|
+
def method_added(method)
|
6
|
+
method = method.to_s.gsub(/_with(out)?_before$/, '')
|
7
|
+
with_method, without_method = "#{method}_with_before", "#{method}_without_before"
|
8
|
+
|
9
|
+
return if method == 'before_each' or method_defined?(with_method)
|
10
|
+
|
11
|
+
define_method(with_method) do |*args, &block|
|
12
|
+
before_each
|
13
|
+
send(without_method, *args, &block)
|
14
|
+
end
|
15
|
+
alias_method_chain(method, :before)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def before_each
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module RestrictiveDestroyer
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
before_destroy :destroyable?
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def acts_as_restrictive_destroyer(options={})
|
10
|
+
raise ArgumentError, "Hash expected, got #{options.class.name}" unless options.is_a?(Hash)
|
11
|
+
class_attribute :restrictive_destroyer_config, :restrictive_destroyer_col_ref
|
12
|
+
self.restrictive_destroyer_config = { column: "deleted_at" }.merge!(options)
|
13
|
+
self.restrictive_destroyer_col_ref = "#{self.table_name}.#{restrictive_destroyer_config[:column]}"
|
14
|
+
scope :undeleted, -> { where("#{restrictive_destroyer_col_ref} IS NULL") }
|
15
|
+
scope :deleted, -> { where("#{restrictive_destroyer_col_ref} IS NOT NULL") }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def readonly?
|
20
|
+
is_destroyed?
|
21
|
+
end
|
22
|
+
|
23
|
+
def restrictive_destroy
|
24
|
+
update_attributes("#{restrictive_destroyer_attr_ref}" => Time.current)
|
25
|
+
end
|
26
|
+
|
27
|
+
def restrictive_destroy_with_api
|
28
|
+
save_and_manage_api("#{restrictive_destroyer_attr_ref}" => Time.current)
|
29
|
+
end
|
30
|
+
|
31
|
+
def is_destroyed?
|
32
|
+
!send("#{restrictive_destroyer_attr_ref}_changed?") && send("#{restrictive_destroyer_attr_ref}?")
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def restrictive_destroyer_attr_ref
|
38
|
+
self.class.restrictive_destroyer_config[:column]
|
39
|
+
end
|
40
|
+
|
41
|
+
def destroyable?
|
42
|
+
false
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Spree
|
2
|
+
class Plan < ActiveRecord::Base
|
3
|
+
module ApiHandler
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
before_validation :set_api_plan_id, on: :create
|
8
|
+
after_create :create_plan
|
9
|
+
after_update :delete_plan, :if => [:deleted_at_changed?, :deleted_at?]
|
10
|
+
after_update :update_plan, :if => :name_changed?
|
11
|
+
end
|
12
|
+
|
13
|
+
def create_plan
|
14
|
+
provider.create_plan(self)
|
15
|
+
end
|
16
|
+
|
17
|
+
def delete_plan
|
18
|
+
provider.delete_plan(self)
|
19
|
+
end
|
20
|
+
|
21
|
+
def update_plan
|
22
|
+
provider.update_plan(self)
|
23
|
+
end
|
24
|
+
|
25
|
+
def save_and_manage_api(*args)
|
26
|
+
begin
|
27
|
+
new_record? ? save : update_attributes(*args)
|
28
|
+
rescue provider.error_class, ActiveRecord::RecordNotFound => e
|
29
|
+
logger.error "Error: #{e.message}"
|
30
|
+
errors.add :base, e.message
|
31
|
+
false
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def provider
|
36
|
+
recurring.present? ? recurring : (raise ActiveRecord::RecordNotFound.new("Provider not found."))
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def set_api_plan_id
|
42
|
+
provider.set_api_plan_id(self)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Spree
|
2
|
+
class Recurring < ActiveRecord::Base
|
3
|
+
class StripeRecurring < Spree::Recurring
|
4
|
+
module ApiHandler
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
include BeforeEach
|
9
|
+
include PlanApiHandler
|
10
|
+
include SubscriptionApiHandler
|
11
|
+
include SubscriptionEventApiHandler
|
12
|
+
end
|
13
|
+
|
14
|
+
def error_class
|
15
|
+
Stripe::InvalidRequestError
|
16
|
+
end
|
17
|
+
|
18
|
+
def raise_invalid_object_error(object, type)
|
19
|
+
raise error_class.new("Not a valid object.") unless object.is_a?(type)
|
20
|
+
end
|
21
|
+
|
22
|
+
def set_api_key
|
23
|
+
Stripe.api_key = preferred_secret_key
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Spree
|
2
|
+
class Recurring < ActiveRecord::Base
|
3
|
+
class StripeRecurring < Spree::Recurring
|
4
|
+
module ApiHandler
|
5
|
+
module PlanApiHandler
|
6
|
+
def create_plan(plan)
|
7
|
+
raise_invalid_object_error(plan, Spree::Plan)
|
8
|
+
Stripe::Plan.create(
|
9
|
+
amount: stripe_amount(plan.amount),
|
10
|
+
interval: plan.interval,
|
11
|
+
interval_count: plan.interval_count,
|
12
|
+
name: plan.name,
|
13
|
+
currency: plan.currency,
|
14
|
+
id: plan.api_plan_id,
|
15
|
+
trial_period_days: plan.trial_period_days
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
def delete_plan(plan)
|
20
|
+
raise_invalid_object_error(plan, Spree::Plan)
|
21
|
+
stripe_plan = retrieve_api_plan(plan)
|
22
|
+
stripe_plan.delete
|
23
|
+
end
|
24
|
+
|
25
|
+
def update_plan(plan)
|
26
|
+
raise_invalid_object_error(plan, Spree::Plan)
|
27
|
+
stripe_plan = retrieve_api_plan(plan)
|
28
|
+
stripe_plan.name = plan.name
|
29
|
+
stripe_plan.save
|
30
|
+
end
|
31
|
+
|
32
|
+
def set_api_plan_id(plan)
|
33
|
+
plan.api_plan_id = "KS-Plan-#{Time.current.to_i}"
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def retrieve_api_plan(plan)
|
39
|
+
Stripe::Plan.retrieve(plan.api_plan_id)
|
40
|
+
end
|
41
|
+
|
42
|
+
def stripe_amount(amount)
|
43
|
+
amount * 100
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/app/models/concerns/spree/recurring/stripe_recurring/api_handler/subscription_api_handler.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
module Spree
|
2
|
+
class Recurring < ActiveRecord::Base
|
3
|
+
class StripeRecurring < Spree::Recurring
|
4
|
+
module ApiHandler
|
5
|
+
module SubscriptionApiHandler
|
6
|
+
def subscribe(subscription)
|
7
|
+
raise_invalid_object_error(subscription, Spree::Subscription)
|
8
|
+
customer = subscription.user.find_or_create_stripe_customer(subscription.card_token)
|
9
|
+
customer.subscriptions.create(plan: subscription.api_plan_id)
|
10
|
+
end
|
11
|
+
|
12
|
+
def unsubscribe(subscription)
|
13
|
+
raise_invalid_object_error(subscription, Spree::Subscription)
|
14
|
+
subscription.user.api_customer.cancel_subscription
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Spree
|
2
|
+
class Recurring < ActiveRecord::Base
|
3
|
+
class StripeRecurring < Spree::Recurring
|
4
|
+
module ApiHandler
|
5
|
+
module SubscriptionEventApiHandler
|
6
|
+
def retrieve_event(event_id)
|
7
|
+
begin
|
8
|
+
Stripe::Event.retrieve(event_id)
|
9
|
+
rescue error_class => e
|
10
|
+
logger.error "Stripe Event error: #{e.message}"
|
11
|
+
errors.add :base, "Stripe Event error: #{e.message}"
|
12
|
+
nil
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Spree
|
2
|
+
class Subscription < ActiveRecord::Base
|
3
|
+
module ApiHandler
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
attr_accessor :card_token
|
8
|
+
before_create :subscribe
|
9
|
+
before_update :unsubscribe, :if => [:unsubscribed_at_changed?, :unsubscribed_at?]
|
10
|
+
end
|
11
|
+
|
12
|
+
def subscribe
|
13
|
+
provider.subscribe(self)
|
14
|
+
self.subscribed_at = Time.current
|
15
|
+
end
|
16
|
+
|
17
|
+
def unsubscribe
|
18
|
+
provider.unsubscribe(self)
|
19
|
+
end
|
20
|
+
|
21
|
+
def save_and_manage_api(*args)
|
22
|
+
begin
|
23
|
+
new_record? ? save : update_attributes(*args)
|
24
|
+
rescue provider.error_class, ActiveRecord::RecordNotFound => e
|
25
|
+
logger.error "Error while subscribing: #{e.message}"
|
26
|
+
errors.add :base, "There was a problem with your credit card"
|
27
|
+
false
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def provider
|
32
|
+
plan.try(:recurring).present? ? plan.recurring : (raise ActiveRecord::RecordNotFound.new("Provider not found."))
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Spree
|
2
|
+
class Subscription < ActiveRecord::Base
|
3
|
+
module RoleSubscriber
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
before_create :add_role_subscriber, :unless => :unsubscribed_at?
|
8
|
+
before_update :remove_role_subscriber, :if => :unsubscribed_at?
|
9
|
+
end
|
10
|
+
|
11
|
+
def add_role_subscriber
|
12
|
+
if role = subscriber_role
|
13
|
+
user.spree_roles << role
|
14
|
+
else
|
15
|
+
errors.add :base, "Subscriber role is not present."
|
16
|
+
false
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def remove_role_subscriber
|
21
|
+
if role = subscriber_role
|
22
|
+
user.spree_roles.destroy(role)
|
23
|
+
else
|
24
|
+
errors.add :base, "Subscriber role is not present."
|
25
|
+
false
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def subscriber_role
|
32
|
+
Spree::Role.where(name: 'subscriber').first
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Spree
|
2
|
+
class Plan < ActiveRecord::Base
|
3
|
+
include RestrictiveDestroyer
|
4
|
+
include ApiHandler
|
5
|
+
|
6
|
+
acts_as_restrictive_destroyer
|
7
|
+
|
8
|
+
belongs_to :recurring
|
9
|
+
has_many :subscriptions
|
10
|
+
|
11
|
+
validates :amount, :interval, :interval_count, :name, :currency, :recurring_id, :api_plan_id, presence: true
|
12
|
+
attr_readonly :amount, :interval, :currency, :id, :trial_period_days, :interval_count, :recurring_id, :api_plan_id
|
13
|
+
|
14
|
+
before_validation :manage_default, if: :default_changed?
|
15
|
+
|
16
|
+
scope :active, -> { undeleted.where(active: true) }
|
17
|
+
scope :visible, -> { active.joins(:recurring).where(["spree_recurrings.active = ? AND spree_recurrings.deleted_at IS NULL", true]) }
|
18
|
+
|
19
|
+
def visible?
|
20
|
+
active? && !is_destroyed? && recurring.visible?
|
21
|
+
end
|
22
|
+
|
23
|
+
def manage_default
|
24
|
+
recurring.plans.undeleted.where.not(id: id).update_all(default: false)
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.default
|
28
|
+
visible.find_by(default: true)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Spree
|
2
|
+
class Recurring < ActiveRecord::Base
|
3
|
+
include RestrictiveDestroyer
|
4
|
+
|
5
|
+
acts_as_restrictive_destroyer
|
6
|
+
|
7
|
+
preference :secret_key, :string
|
8
|
+
preference :public_key, :string
|
9
|
+
|
10
|
+
has_many :plans
|
11
|
+
attr_readonly :type
|
12
|
+
validates :type, :name, presence: true
|
13
|
+
scope :active, -> { undeleted.where(active: true) }
|
14
|
+
|
15
|
+
def self.display_name
|
16
|
+
name.gsub(%r{.+:}, '')
|
17
|
+
end
|
18
|
+
|
19
|
+
def visible?
|
20
|
+
active? && !is_destroyed?
|
21
|
+
end
|
22
|
+
|
23
|
+
def default_plan
|
24
|
+
plans.default
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|