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
data/LICENSE ADDED
@@ -0,0 +1,26 @@
1
+ Copyright (c) 2013 [name of plugin creator]
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification,
5
+ are permitted provided that the following conditions are met:
6
+
7
+ * Redistributions of source code must retain the above copyright notice,
8
+ this list of conditions and the following disclaimer.
9
+ * Redistributions in binary form must reproduce the above copyright notice,
10
+ this list of conditions and the following disclaimer in the documentation
11
+ and/or other materials provided with the distribution.
12
+ * Neither the name Spree nor the names of its contributors may be used to
13
+ endorse or promote products derived from this software without specific
14
+ prior written permission.
15
+
16
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
20
+ CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
21
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
22
+ PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
23
+ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
24
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
25
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
26
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md ADDED
@@ -0,0 +1,108 @@
1
+ Spree Account Recurring [![Code Climate](https://codeclimate.com/github/vinsol/spree-account-recurring.png)](https://codeclimate.com/github/vinsol/spree-account-recurring) [![Build Status](https://travis-ci.org/vinsol/spree-account-recurring.svg?branch=2-1-stable)](https://travis-ci.org/vinsol/spree-account-recurring)
2
+ =========================
3
+
4
+ Spree extension to manage recurring payments/subscriptions using [Stripe Payment Gateway](https://stripe.com/).
5
+
6
+ All plans and subscription scenarios are been managed as per [Stripe Docs](https://stripe.com/docs/api)
7
+
8
+ Installation
9
+ ------------
10
+
11
+ Install `spree_account_recurring` by adding the following to your `Gemfile`:
12
+
13
+ ```ruby
14
+ gem 'spree_account_recurring'
15
+ ```
16
+
17
+ Bundle your dependencies and run the installation generator:
18
+
19
+ ```shell
20
+ bundle
21
+ bundle exec rails g spree_account_recurring:install
22
+ ```
23
+
24
+ Usage
25
+ -----
26
+
27
+ At Admin end this will add a configuration tab as "Recurring".
28
+
29
+ * Creating a Recurring Provider:
30
+ * Create a recurring using `Spree::Recurring::StripeRecurring` Provider and save
31
+ * Add secret key and public key provided by [stripe](https://stripe.com/) to this recurring.
32
+
33
+ * Creating Plans for Recurring Provider:
34
+ * Go to "Manage Plans" from Recurring edit page.
35
+ * Create a plan by specifying respective details. This will create the same plan on your stripe account.
36
+ * Only name can be updated for a plan.
37
+
38
+ One Recurring Provider can have multiple plans.
39
+
40
+ At Front end you can view all plans here: `http://your.domain.name/recurring/plans`
41
+
42
+ * Subscribe a plan:
43
+ * Click subscribe for any plan.
44
+ * Fill in credit card details and submit.
45
+ * This will create a customer in Stripe for user and subscribe that user respective to plan.
46
+
47
+ * Unsubscribe a plan:
48
+ * In plans page subscribed plan will be listed and from there user can unsubscribe from plan.
49
+
50
+ At Admin, all subscriptions can be seen under "Reports" -> "Subscriptions".
51
+
52
+ Stripe Webhook
53
+ --------------
54
+
55
+ Create a webhook at stripe with url `http://your.domain.name/recurring_hooks/handler` which will receive below mentioned stripe event hooks.
56
+
57
+ Events:
58
+ * `customer.subscription.deleted`
59
+ * `customer.subscription.created`
60
+ * `customer.subscription.updated`
61
+ * `invoice.payment_succeeded`
62
+ * `invoice.payment_failed`
63
+ * `charge.succeeded`
64
+ * `charge.failed`
65
+ * `charge.refunded`
66
+ * `charge.captured`
67
+ * `plan.created`
68
+ * `plan.updated`
69
+ * `plan.deleted`
70
+
71
+ These events can be viewed at admin in "Reports" -> "Subscription Events"
72
+
73
+ Testing
74
+ -------
75
+
76
+ Be sure to bundle your dependencies and then create a dummy test app for the specs to run against.
77
+
78
+ ```shell
79
+ bundle
80
+ bundle exec rake test_app
81
+ bundle exec rspec spec
82
+ ```
83
+
84
+ When testing your applications integration with this extension you may use it's factories.
85
+ Simply add this require statement to your spec_helper:
86
+
87
+ ```ruby
88
+ require 'spree_account_recurring/factories'
89
+ ```
90
+
91
+ Contributing
92
+ ------------
93
+
94
+ 1. Fork the repo.
95
+ 2. Clone your repo.
96
+ 3. Run `bundle install`.
97
+ 4. Run `bundle exec rake test_app` to create the test application in `spec/test_app`.
98
+ 5. Make your changes.
99
+ 6. Ensure specs pass by running `bundle exec rspec spec`.
100
+ 7. Submit your pull request.
101
+
102
+
103
+ Credits
104
+ -------
105
+
106
+ [![vinsol.com: Ruby on Rails, iOS and Android developers](http://vinsol.com/vin_logo.png "Ruby on Rails, iOS and Android developers")](http://vinsol.com)
107
+
108
+ Copyright (c) 2014 [vinsol.com](http://vinsol.com "Ruby on Rails, iOS and Android developers"), released under the New MIT License
@@ -0,0 +1 @@
1
+ //= require admin/spree_backend
@@ -0,0 +1 @@
1
+ //= require store/spree_frontend
@@ -0,0 +1,53 @@
1
+ // Inspired by https://stripe.com/docs/stripe.js
2
+
3
+ mapCC = function(ccType){
4
+ if(ccType == 'MasterCard'){
5
+ return 'mastercard';
6
+ } else if(ccType == 'Visa'){
7
+ return 'visa';
8
+ } else if(ccType == 'American Express'){
9
+ return 'amex';
10
+ } else if(ccType == 'Discover'){
11
+ return 'discover';
12
+ } else if(ccType == 'Diners Club'){
13
+ return 'dinersclub';
14
+ } else if(ccType == 'JCB'){
15
+ return 'jcb';
16
+ }
17
+ }
18
+
19
+ $(document).ready(function(){
20
+ // For errors that happen later.
21
+ Spree.stripePaymentMethod.prepend("<div id='stripeError' class='errorExplanation' style='display:none'></div>")
22
+
23
+ $('.continue').on('click', function(){
24
+ $('#stripeError').hide();
25
+ if(Spree.stripePaymentMethod.is(':visible')){
26
+ expiration = $('.cardExpiry:visible').payment('cardExpiryVal')
27
+ params = {
28
+ number: $('.cardNumber:visible').val(),
29
+ cvc: $('.cardCode:visible').val(),
30
+ exp_month: expiration.month || 0,
31
+ exp_year: expiration.year || 0
32
+ };
33
+
34
+ Stripe.card.createToken(params, stripeResponseHandler);
35
+ return false;
36
+ }
37
+ });
38
+ });
39
+
40
+ stripeResponseHandler = function(status, response){
41
+ if(response.error){
42
+ $('#stripeError').html(response.error.message);
43
+ $('#stripeError').show();
44
+ } else {
45
+ Spree.stripePaymentMethod.find('#card_number, #card_expiry, #card_code').prop("disabled" , true);
46
+ Spree.stripePaymentMethod.find(".ccType").prop("disabled", false);
47
+ Spree.stripePaymentMethod.find(".ccType").val(mapCC(response.card.type))
48
+ token = response['id'];
49
+ // insert the token into the form so it gets submitted to the server
50
+ Spree.stripePaymentMethod.append("<input type='hidden' class='stripeToken' name='subscription[card_token]' value='" + token + "'/>");
51
+ Spree.stripePaymentMethod.parents("form").get(0).submit();
52
+ }
53
+ }
@@ -0,0 +1,3 @@
1
+ /*
2
+ *= require admin/spree_backend
3
+ */
@@ -0,0 +1,3 @@
1
+ /*
2
+ *= require store/spree_frontend
3
+ */
@@ -0,0 +1,47 @@
1
+ module Spree
2
+ module Admin
3
+ module RansackDateSearch
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+ def ransack_date_searchable(options={})
8
+ raise ArgumentError, "Hash expected, got #{options.class.name}" unless options.is_a?(Hash)
9
+ class_attribute :ransack_date_search_config, :ransack_date_search_col_ref, :ransack_date_search_param_gt, :ransack_date_search_param_lt
10
+ self.ransack_date_search_config = { date_col: "created_at" }.merge!(options)
11
+ # self.ransack_date_search_config[:before_action] = [ransack_date_search_config[:before_action]] unless ransack_date_search_config[:before_action].is_a?(Array)
12
+ self.ransack_date_search_col_ref = ransack_date_search_config[:date_col]
13
+ self.ransack_date_search_param_gt = "#{ransack_date_search_col_ref}_gt"
14
+ self.ransack_date_search_param_lt = "#{ransack_date_search_col_ref}_lt"
15
+ end
16
+ end
17
+
18
+ included do
19
+ before_action :parse_ransack_date_search_param!, only: 'index'
20
+ end
21
+
22
+ private
23
+
24
+ def parse_ransack_date_search_param!
25
+ params[:q] = {} unless params[:q]
26
+ parse_ransack_date_search_param_gt!
27
+ parse_ransack_date_search_param_lt!
28
+ params[:q][:s] ||= "#{ransack_date_search_col_ref} desc"
29
+ params[:q].delete_if{ |k, v| v.blank? }
30
+ end
31
+
32
+ def parse_ransack_date_search_param_gt!
33
+ if params[:q][ransack_date_search_param_gt].blank?
34
+ params[:q][ransack_date_search_param_gt] = Time.current.beginning_of_month
35
+ else
36
+ params[:q][ransack_date_search_param_gt] = Time.zone.parse(params[:q][ransack_date_search_param_gt]).beginning_of_day rescue Time.current.beginning_of_month
37
+ end
38
+ end
39
+
40
+ def parse_ransack_date_search_param_lt!
41
+ if params[:q][ransack_date_search_param_lt].present?
42
+ params[:q][ransack_date_search_param_lt] = Time.zone.parse(params[:q][ransack_date_search_param_lt]).end_of_day rescue ""
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,63 @@
1
+ module Spree
2
+ module Admin
3
+ class PlansController < Spree::Admin::BaseController
4
+ before_action :load_recurring
5
+ before_action :find_plan, :only => [:edit, :destroy, :update]
6
+
7
+ def index
8
+ @plans = Spree::Plan.undeleted.order('id desc')
9
+ end
10
+
11
+ def new
12
+ @plan = @recurring.plans.build
13
+ end
14
+
15
+ def create
16
+ @plan = @recurring.plans.build(plan_params)
17
+ if @plan.save_and_manage_api
18
+ flash[:notice] = 'Plan created successfully.'
19
+ redirect_to edit_admin_recurring_plan_path(@recurring, @plan)
20
+ else
21
+ render :new
22
+ end
23
+ end
24
+
25
+ def update
26
+ if @plan.save_and_manage_api(plan_params(:update))
27
+ flash[:notice] = 'Plan updated successfully.'
28
+ redirect_to edit_admin_recurring_plan_path(@recurring, @plan)
29
+ else
30
+ render :edit
31
+ end
32
+ end
33
+
34
+ def destroy
35
+ @plan.restrictive_destroy_with_api
36
+ end
37
+
38
+ private
39
+
40
+ def load_recurring
41
+ unless @recurring = Spree::Recurring.undeleted.where(id: params[:recurring_id]).first
42
+ flash[:error] = "Recurring not found."
43
+ redirect_to admin_recurrings_path
44
+ end
45
+ end
46
+
47
+ def plan_params(action=:create)
48
+ if action == :create
49
+ params.require(:plan).permit(:name, :trial_period_days, :interval, :currency, :amount, :active, :interval_count, :default)
50
+ else
51
+ params.require(:plan).permit(:name, :active, :default)
52
+ end
53
+ end
54
+
55
+ def find_plan
56
+ unless @plan = @recurring.plans.undeleted.where(id: params[:id]).first
57
+ flash[:error] = "Plan not found."
58
+ redirect_to admin_recurring_plans_path(@recurring)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,66 @@
1
+ module Spree
2
+ module Admin
3
+ class RecurringsController < Spree::Admin::BaseController
4
+ before_action :find_recurring, :only => [:edit, :update, :destroy]
5
+ before_action :build_recurring, :only => :create
6
+
7
+ def index
8
+ @recurrings = Spree::Recurring.undeleted.order('id desc')
9
+ end
10
+
11
+ def new
12
+ @recurring = Spree::Recurring.new
13
+ end
14
+
15
+ def create
16
+ if @recurring.save
17
+ flash[:notice] = "Recurring created succesfully."
18
+ redirect_to edit_admin_recurring_url(@recurring)
19
+ else
20
+ render :new
21
+ end
22
+ end
23
+
24
+ def update
25
+ if @recurring.update_attributes(recurring_params(:update))
26
+ flash[:notice] = "Recurring updated succesfully."
27
+ redirect_to edit_admin_recurring_url(@recurring)
28
+ else
29
+ render :edit
30
+ end
31
+ end
32
+
33
+ def destroy
34
+ @recurring.restrictive_destroy
35
+ end
36
+
37
+ private
38
+
39
+ def find_recurring
40
+ unless @recurring = Spree::Recurring.undeleted.where(id: params[:id]).first
41
+ flash[:error] = "Recurring not found."
42
+ respond_to do |format|
43
+ format.html {redirect_to admin_recurrings_url}
44
+ format.js { }
45
+ end
46
+ end
47
+ end
48
+
49
+ def recurring_params(action=:create)
50
+ if action == :create
51
+ params.require(:recurring).permit(:name, :type, :description, :active)
52
+ else
53
+ params.require(:recurring).permit(:name, :description, :active).merge(preference_params)
54
+ end
55
+ end
56
+
57
+ def build_recurring
58
+ @recurring = recurring_params.delete(:type).constantize.new(recurring_params)
59
+ end
60
+
61
+ def preference_params
62
+ params[ActiveModel::Naming.param_key(@recurring)]
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,14 @@
1
+ module Spree
2
+ module Admin
3
+ class SubscriptionEventsController < Spree::Admin::BaseController
4
+ include RansackDateSearch
5
+ ransack_date_searchable date_col: 'created_at'
6
+
7
+ def index
8
+ @search = Spree::SubscriptionEvent.ransack(params[:q])
9
+ @subscription_events = @search.result.includes(subscription: { plan: :recurring }).references(subscription: { plan: :recurring }).page(params[:page]).per(15)
10
+ respond_with(@subscription_events)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ module Spree
2
+ module Admin
3
+ class SubscriptionsController < Spree::Admin::BaseController
4
+ include RansackDateSearch
5
+ ransack_date_searchable date_col: 'subscribed_at'
6
+
7
+ def index
8
+ @search = Spree::Subscription.ransack(params[:q])
9
+ @subscriptions = @search.result.includes(plan: :recurring).references(plan: :recurring).page(params[:page]).per(15)
10
+ respond_with(@subscriptions)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,7 @@
1
+ module Spree
2
+ class PlansController < StoreController
3
+ def index
4
+ @plans = Spree::Plan.visible.order('id desc')
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,53 @@
1
+ module Spree
2
+ class RecurringHooksController < BaseController
3
+ skip_before_filter :verify_authenticity_token
4
+
5
+ before_action :authenticate_webhook
6
+ before_action :find_subscription
7
+
8
+ respond_to :json
9
+
10
+ def handler
11
+ @subscription_event = @subscription.events.build(subscription_event_params)
12
+ if @subscription_event.save
13
+ render_status_ok
14
+ else
15
+ render_status_failure
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def event
22
+ @event ||= (Rails.env.production? ? params.deep_dup : params.deep_dup[:recurring_hook])
23
+ end
24
+
25
+ def authenticate_webhook
26
+ render_status_ok if event.blank? || (event[:livemode] != Rails.env.production?) || (!Spree::Recurring::StripeRecurring::WEBHOOKS.include?(event[:type]))
27
+ end
28
+
29
+ def find_subscription
30
+ render_status_ok unless @subscription = Spree::User.find_by(stripe_customer_id: event[:data][:object][:customer]).subscription
31
+ end
32
+
33
+ def retrieve_api_event
34
+ @event = @subscription.provider.retrieve_event(event[:id])
35
+ end
36
+
37
+ def subscription_event_params
38
+ if retrieve_api_event && event.data.object.customer == @subscription.user.stripe_customer_id
39
+ { event_id: event.id, request_type: event.type, response: event.to_json }
40
+ else
41
+ {}
42
+ end
43
+ end
44
+
45
+ def render_status_ok
46
+ render text: '', status: 200
47
+ end
48
+
49
+ def render_status_failure
50
+ render text: '', status: 403
51
+ end
52
+ end
53
+ end