spree_account_recurring 1.0.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.
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