spree_account_recurring 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. data/LICENSE +26 -0
  2. data/README.md +108 -0
  3. data/app/assets/javascripts/admin/spree_account_recurring.js +1 -0
  4. data/app/assets/javascripts/store/spree_account_recurring.js +1 -0
  5. data/app/assets/javascripts/store/stripe.js +53 -0
  6. data/app/assets/stylesheets/admin/spree_account_recurring.css +3 -0
  7. data/app/assets/stylesheets/store/spree_account_recurring.css +3 -0
  8. data/app/controllers/concerns/spree/admin/ransack_date_search.rb +47 -0
  9. data/app/controllers/spree/admin/plans_controller.rb +63 -0
  10. data/app/controllers/spree/admin/recurrings_controller.rb +66 -0
  11. data/app/controllers/spree/admin/subscription_events_controller.rb +14 -0
  12. data/app/controllers/spree/admin/subscriptions_controller.rb +14 -0
  13. data/app/controllers/spree/plans_controller.rb +7 -0
  14. data/app/controllers/spree/recurring_hooks_controller.rb +53 -0
  15. data/app/controllers/spree/subscriptions_controller.rb +69 -0
  16. data/app/models/concerns/before_each.rb +21 -0
  17. data/app/models/concerns/restrictive_destroyer.rb +44 -0
  18. data/app/models/concerns/spree/plan/api_handler.rb +46 -0
  19. data/app/models/concerns/spree/recurring/stripe_recurring/api_handler.rb +28 -0
  20. data/app/models/concerns/spree/recurring/stripe_recurring/api_handler/plan_api_handler.rb +49 -0
  21. data/app/models/concerns/spree/recurring/stripe_recurring/api_handler/subscription_api_handler.rb +20 -0
  22. data/app/models/concerns/spree/recurring/stripe_recurring/api_handler/subscription_event_api_handler.rb +19 -0
  23. data/app/models/concerns/spree/subscription/api_handler.rb +38 -0
  24. data/app/models/concerns/spree/subscription/role_subscriber.rb +36 -0
  25. data/app/models/spree/plan.rb +31 -0
  26. data/app/models/spree/recurring.rb +27 -0
  27. data/app/models/spree/recurring/stripe_recurring.rb +16 -0
  28. data/app/models/spree/subscriber_ability.rb +17 -0
  29. data/app/models/spree/subscription.rb +33 -0
  30. data/app/models/spree/subscription_event.rb +11 -0
  31. data/app/models/spree/user_decorator.rb +14 -0
  32. data/app/overrides/admin/view_decorator.rb +27 -0
  33. data/app/views/spree/admin/plans/_form.html.erb +47 -0
  34. data/app/views/spree/admin/plans/destroy.js.erb +0 -0
  35. data/app/views/spree/admin/plans/edit.html.erb +22 -0
  36. data/app/views/spree/admin/plans/index.html.erb +70 -0
  37. data/app/views/spree/admin/plans/new.html.erb +22 -0
  38. data/app/views/spree/admin/recurrings/_form.html.erb +42 -0
  39. data/app/views/spree/admin/recurrings/destroy.js.erb +0 -0
  40. data/app/views/spree/admin/recurrings/edit.html.erb +23 -0
  41. data/app/views/spree/admin/recurrings/index.html.erb +48 -0
  42. data/app/views/spree/admin/recurrings/new.html.erb +22 -0
  43. data/app/views/spree/admin/subscription_events/index.html.erb +72 -0
  44. data/app/views/spree/admin/subscriptions/index.html.erb +76 -0
  45. data/app/views/spree/plans/index.html.erb +14 -0
  46. data/app/views/spree/subscriptions/new.html.erb +36 -0
  47. data/app/views/spree/subscriptions/show.html.erb +4 -0
  48. data/config/initializers/stripe.rb +2 -0
  49. data/config/locales/en.yml +43 -0
  50. data/config/routes.rb +24 -0
  51. data/db/migrate/20131119054112_create_spree_recurring.rb +13 -0
  52. data/db/migrate/20131119054212_create_spree_plan.rb +21 -0
  53. data/db/migrate/20131119054322_create_spree_subscription.rb +16 -0
  54. data/db/migrate/20131125123820_create_spree_subscription_events.rb +14 -0
  55. data/db/migrate/20131202110012_add_subscriber_role_to_spree_roles.rb +15 -0
  56. data/db/migrate/20140301141327_add_stripe_customer_id_to_spree_users.rb +6 -0
  57. data/db/migrate/20140303072857_add_default_to_spree_plans.rb +6 -0
  58. data/db/migrate/20140319092254_add_response_to_spree_subscription_events.rb +5 -0
  59. data/lib/generators/spree_account_recurring/install/install_generator.rb +31 -0
  60. data/lib/spree_account_recurring.rb +2 -0
  61. data/lib/spree_account_recurring/engine.rb +22 -0
  62. data/lib/spree_account_recurring/factories.rb +6 -0
  63. data/lib/spree_account_recurring/testing_support/ransack_date_search_helper.rb +125 -0
  64. 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
@@ -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