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