spree_stripe_subscriptions 0.0.1

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 (61) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +91 -0
  3. data/.rubocop.yml +26 -0
  4. data/.travis.yml +37 -0
  5. data/Gemfile +12 -0
  6. data/LICENSE +26 -0
  7. data/README.md +72 -0
  8. data/Rakefile +21 -0
  9. data/app/controllers/spree/admin/stripe_configurations_controller.rb +6 -0
  10. data/app/controllers/spree/admin/stripe_customers_controller.rb +6 -0
  11. data/app/controllers/spree/admin/stripe_plans_controller.rb +36 -0
  12. data/app/controllers/spree/admin/stripe_subscription_events_controller.rb +6 -0
  13. data/app/controllers/spree/admin/stripe_subscriptions_controller.rb +6 -0
  14. data/app/controllers/spree/stripe_plans_controller.rb +20 -0
  15. data/app/controllers/spree/stripe_subscriptions_controller.rb +166 -0
  16. data/app/controllers/spree/stripe_webhooks_controller.rb +56 -0
  17. data/app/models/spree/stripe_configuration.rb +28 -0
  18. data/app/models/spree/stripe_customer.rb +30 -0
  19. data/app/models/spree/stripe_invoice.rb +21 -0
  20. data/app/models/spree/stripe_plan.rb +104 -0
  21. data/app/models/spree/stripe_subscription.rb +167 -0
  22. data/app/models/spree/stripe_subscription_event.rb +10 -0
  23. data/app/models/spree_stripe_subscriptions/configuration.rb +15 -0
  24. data/app/models/spree_stripe_subscriptions/user_decorator.rb +50 -0
  25. data/app/overrides/spree/admin/shared/_main_menu.rb +13 -0
  26. data/app/serializers/.gitkeep +0 -0
  27. data/app/services/.gitkeep +0 -0
  28. data/app/views/spree/admin/shared/sub_menu/_stripe_subscriptions.html.erb +8 -0
  29. data/app/views/spree/admin/stripe_configurations/_form.html.erb +42 -0
  30. data/app/views/spree/admin/stripe_configurations/edit.html.erb +17 -0
  31. data/app/views/spree/admin/stripe_configurations/index.html.erb +44 -0
  32. data/app/views/spree/admin/stripe_configurations/new.html.erb +13 -0
  33. data/app/views/spree/admin/stripe_customers/index.html.erb +36 -0
  34. data/app/views/spree/admin/stripe_plans/_form.html.erb +50 -0
  35. data/app/views/spree/admin/stripe_plans/edit.html.erb +15 -0
  36. data/app/views/spree/admin/stripe_plans/index.html.erb +59 -0
  37. data/app/views/spree/admin/stripe_plans/new.html.erb +15 -0
  38. data/app/views/spree/admin/stripe_subscription_events/index.html.erb +36 -0
  39. data/app/views/spree/admin/stripe_subscriptions/index.html.erb +40 -0
  40. data/app/views/spree/stripe_plans/index.html.erb +77 -0
  41. data/bin/rails +8 -0
  42. data/config/initializers/stripe.rb +11 -0
  43. data/config/locales/en.yml +80 -0
  44. data/config/routes.rb +27 -0
  45. data/db/migrate/20221130094411_add_spree_stripe_configuration_model.rb +14 -0
  46. data/db/migrate/20221130095015_add_spree_stripe_plan_model.rb +23 -0
  47. data/db/migrate/20221130100526_add_spree_stripe_customer.rb +11 -0
  48. data/db/migrate/20221130100903_add_spree_stripe_subscription_model.rb +23 -0
  49. data/db/migrate/20221130102650_add_spree_stripe_subscription_events_model.rb +16 -0
  50. data/db/migrate/20221202111450_add_weightage_to_stripe_plan.rb +10 -0
  51. data/db/migrate/20221209065244_add_schedules_to_subscription.rb +6 -0
  52. data/db/migrate/20221209094013_add_stripe_invoices.rb +33 -0
  53. data/db/migrate/20221209131817_add_user_to_invoice.rb +5 -0
  54. data/db/migrate/20221221080141_add_extra_plan_fields.rb +8 -0
  55. data/lib/generators/spree_stripe_subscriptions/install/install_generator.rb +20 -0
  56. data/lib/spree_stripe_subscriptions/engine.rb +24 -0
  57. data/lib/spree_stripe_subscriptions/factories.rb +6 -0
  58. data/lib/spree_stripe_subscriptions/version.rb +11 -0
  59. data/lib/spree_stripe_subscriptions.rb +4 -0
  60. data/spree_stripe_subscriptions.gemspec +34 -0
  61. metadata +207 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9085cdba71a500882f7360cb06f86ef9d9ee5617c52440e6c02c57e8a04218d6
4
+ data.tar.gz: d4891c7b4184367479b884b3cb740273b62d5fbcb1a18de4e3eb5ebeb81953db
5
+ SHA512:
6
+ metadata.gz: 693ad9f3a7739fdd12d52410ec29ecc0036106cea56e4ee2edef986ff53e64ec5ddfc40184c03f9945eabe15c112f0aa5e2ddd252a2f351f2fe2861eaecfbecd
7
+ data.tar.gz: 5f4d4c7ba7b99676bd735834178a6aaf4f28f7793d13eaa465d2dd9fe3b7daa15b8fa5ae158fdd3659f1c1917e9fe4345baaef8948f74c7d9e02033b4969c91a
data/.gitignore ADDED
@@ -0,0 +1,91 @@
1
+ *.rbc
2
+ capybara-*.html
3
+ .rspec
4
+ /db/*.sqlite3
5
+ /db/*.sqlite3-journal
6
+ /db/*.sqlite3-[0-9]*
7
+ /public/system
8
+ /coverage/
9
+ /spec/tmp
10
+ *.orig
11
+ rerun.txt
12
+ pickle-email-*.html
13
+
14
+ # Ignore all logfiles and tempfiles.
15
+ /log/*
16
+ /tmp/*
17
+ !/log/.keep
18
+ !/tmp/.keep
19
+
20
+ # TODO Comment out this rule if you are OK with secrets being uploaded to the repo
21
+ config/initializers/secret_token.rb
22
+ config/master.key
23
+
24
+ # Only include if you have production secrets in this file, which is no longer a Rails default
25
+ # config/secrets.yml
26
+
27
+ # dotenv
28
+ # TODO Comment out this rule if environment variables can be committed
29
+ .env
30
+
31
+ ## Environment normalization:
32
+ /.bundle
33
+ /vendor/bundle
34
+
35
+ # these should all be checked in to normalize the environment:
36
+ # Gemfile.lock, .ruby-version, .ruby-gemset
37
+
38
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
39
+ .rvmrc
40
+
41
+ # if using bower-rails ignore default bower_components path bower.json files
42
+ /vendor/assets/bower_components
43
+ *.bowerrc
44
+ bower.json
45
+
46
+ # Ignore pow environment settings
47
+ .powenv
48
+
49
+ # Ignore Byebug command history file.
50
+ .byebug_history
51
+
52
+ # Ignore node_modules
53
+ node_modules/
54
+
55
+ # Ignore precompiled javascript packs
56
+ /public/packs
57
+ /public/packs-test
58
+ /public/assets
59
+
60
+ # Ignore yarn files
61
+ /yarn-error.log
62
+ yarn-debug.log*
63
+ .yarn-integrity
64
+
65
+ # Ignore uploaded files in development
66
+ /storage/*
67
+ !/storage/.keep
68
+
69
+ # From Spree Extension
70
+ \#*
71
+ *~
72
+ .#*
73
+ .DS_Store
74
+ .idea
75
+ .localeapp/locales
76
+ .project
77
+ .vscode
78
+ coverage
79
+ default
80
+ Gemfile.lock
81
+ tmp
82
+ nbproject
83
+ pkg
84
+ *.sw?
85
+ spec/dummy
86
+ .sass-cache
87
+ public/spree
88
+ .ruby-version
89
+ .ruby-gemset
90
+ *.gem
91
+ */*.gem
data/.rubocop.yml ADDED
@@ -0,0 +1,26 @@
1
+ require: rubocop-rails
2
+
3
+ AllCops:
4
+ DisplayCopNames: true
5
+ TargetRubyVersion: 2.5
6
+ Include:
7
+ - '**/Gemfile'
8
+ - '**/Rakefile'
9
+ - '**/Appraisals'
10
+ Exclude:
11
+ - 'spec/dummy/**/*'
12
+ - 'lib/generators/**/*'
13
+
14
+ Rails:
15
+ Enabled: true
16
+
17
+ Layout/LineLength:
18
+ Max: 150
19
+
20
+ # DISABLED
21
+
22
+ Style/Documentation:
23
+ Enabled: false
24
+
25
+ Style/FrozenStringLiteralComment:
26
+ Enabled: false
data/.travis.yml ADDED
@@ -0,0 +1,37 @@
1
+ os: linux
2
+ dist: bionic
3
+
4
+ addons:
5
+ apt:
6
+ sources:
7
+ - google-chrome
8
+ packages:
9
+ - google-chrome-stable
10
+
11
+ services:
12
+ - mysql
13
+ - postgresql
14
+
15
+ language: ruby
16
+
17
+ rvm:
18
+ - 2.7
19
+ - 3.0
20
+
21
+ env:
22
+ - DB=mysql
23
+ - DB=postgres
24
+
25
+ before_install:
26
+ - mysql -u root -e "GRANT ALL ON *.* TO 'travis'@'%';"
27
+
28
+ before_script:
29
+ - CHROME_MAIN_VERSION=`google-chrome-stable --version | sed -E 's/(^Google Chrome |\.[0-9]+ )//g'`
30
+ - CHROMEDRIVER_VERSION=`curl -s "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_$CHROME_MAIN_VERSION"`
31
+ - curl "https://chromedriver.storage.googleapis.com/${CHROMEDRIVER_VERSION}/chromedriver_linux64.zip" -O
32
+ - unzip chromedriver_linux64.zip -d ~/bin
33
+ - nvm install 16
34
+
35
+ script:
36
+ - bundle exec rake test_app
37
+ - bundle exec rspec
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source 'https://rubygems.org'
2
+
3
+ git_source(:github) do |repo_name|
4
+ repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?('/')
5
+ "https://github.com/#{repo_name}.git"
6
+ end
7
+
8
+ # gem 'spree', github: 'spree/spree', branch: 'main'
9
+ # gem 'spree_backend', github: 'spree/spree', branch: 'main'
10
+ gem 'rails-controller-testing'
11
+
12
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,26 @@
1
+ Copyright (c) 2022 [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,72 @@
1
+ # SpreeStripeSubscriptions
2
+
3
+ Spree extension to manage stripe subscriptions using Stripe Checkout Session.
4
+
5
+ ## Installation
6
+
7
+ 1. Add this extension to your Gemfile with this line:
8
+
9
+ ```ruby
10
+ gem 'spree_stripe_subscriptions'
11
+ ```
12
+
13
+ 2. Install the gem using Bundler
14
+
15
+ ```ruby
16
+ bundle install
17
+ ```
18
+
19
+ 3. Copy & run migrations
20
+
21
+ ```ruby
22
+ bundle exec rails g spree_stripe_subscriptions:install
23
+ ```
24
+
25
+ 4. Restart your server
26
+
27
+ If your server was running, restart it so that it can find the assets properly.
28
+
29
+ ## Configurations
30
+
31
+ ### Customise Stripe Plans URL
32
+
33
+ To use customise URls, you can set the following configurations:
34
+
35
+ ```ruby
36
+ # In initializers/spree_stripe_subscriptions.rb
37
+ SpreeStripeSubscriptions::Config.stripe_plans_path = 'pricing' # Default url is 'stripe_plans'
38
+ SpreeStripeSubscriptions::Config.stripe_webhooks_path = 'webhook' # Default url is 'stripe_webhooks'
39
+ ```
40
+
41
+ ## Testing
42
+
43
+ First bundle your dependencies, then run `rake`. `rake` will default to building the dummy app if it does not exist, then it will run specs. The dummy app can be regenerated by using `rake test_app`.
44
+
45
+ ```shell
46
+ bundle update
47
+ bundle exec rake
48
+ ```
49
+
50
+ When testing your applications integration with this extension you may use it's factories.
51
+ Simply add this require statement to your spec_helper:
52
+
53
+ ```ruby
54
+ require 'spree_stripe_subscriptions/factories'
55
+ ```
56
+
57
+ ## Releasing
58
+
59
+ ```shell
60
+ bundle exec gem bump -p -t
61
+ bundle exec gem release
62
+ ```
63
+
64
+ For more options please see [gem-release REAMDE](https://github.com/svenfuchs/gem-release)
65
+
66
+ ## Contributing
67
+
68
+ If you'd like to contribute, please take a look at the
69
+ [instructions](CONTRIBUTING.md) for installing dependencies and crafting a good
70
+ pull request.
71
+
72
+ Copyright (c) 2022 [name of extension creator], released under the New BSD License
data/Rakefile ADDED
@@ -0,0 +1,21 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rspec/core/rake_task'
5
+ require 'spree/testing_support/extension_rake'
6
+
7
+ RSpec::Core::RakeTask.new
8
+
9
+ task :default do
10
+ if Dir['spec/dummy'].empty?
11
+ Rake::Task[:test_app].invoke
12
+ Dir.chdir('../../')
13
+ end
14
+ Rake::Task[:spec].invoke
15
+ end
16
+
17
+ desc 'Generates a dummy app for testing'
18
+ task :test_app do
19
+ ENV['LIB_NAME'] = 'spree_stripe_subscriptions'
20
+ Rake::Task['extension:test_app'].invoke
21
+ end
@@ -0,0 +1,6 @@
1
+ module Spree
2
+ module Admin
3
+ class StripeConfigurationsController < Spree::Admin::ResourceController
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Spree
2
+ module Admin
3
+ class StripeCustomersController < Spree::Admin::ResourceController
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,36 @@
1
+ module Spree
2
+ module Admin
3
+ class StripePlansController < Spree::Admin::ResourceController
4
+ before_action :load_strip_configuration
5
+
6
+ def new
7
+ @stripe_plan = @stripe_configuration.stripe_plans.build
8
+ end
9
+
10
+ def collection
11
+ super.reorder(:weightage)
12
+ end
13
+
14
+ def collection_url
15
+ spree.admin_stripe_configuration_stripe_plans_url(@stripe_configuration)
16
+ end
17
+
18
+ def new_object_url
19
+ spree.new_admin_stripe_configuration_stripe_plan_url(@stripe_configuration)
20
+ end
21
+
22
+ private
23
+
24
+ def stripe_configuration
25
+ @stripe_configuration ||= Spree::StripeConfiguration.where(id: params[:stripe_configuration_id]).first
26
+ end
27
+
28
+ def load_strip_configuration
29
+ return if stripe_configuration
30
+
31
+ flash[:error] = I18n.t('spree_stripe_subscriptions.errors.stripe_configuration_doesnt_exist')
32
+ redirect_to admin_stripe_configurations_path
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,6 @@
1
+ module Spree
2
+ module Admin
3
+ class StripeSubscriptionEventsController < Spree::Admin::ResourceController
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module Spree
2
+ module Admin
3
+ class StripeSubscriptionsController < Spree::Admin::ResourceController
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,20 @@
1
+ module Spree
2
+ class StripePlansController < StoreController
3
+ before_action :load_active_plans, only: :index
4
+ before_action :load_user_subscriptions, only: :index
5
+
6
+ def index; end
7
+
8
+ private
9
+
10
+ def load_active_plans
11
+ @stripe_plans = Spree::StripePlan.active.order(:weightage)
12
+ end
13
+
14
+ def load_user_subscriptions
15
+ @active_subscription = if spree_current_user.present?
16
+ spree_current_user.stripe_subscriptions.active.last
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,166 @@
1
+ module Spree
2
+ class StripeSubscriptionsController < StoreController
3
+ before_action :login_required
4
+ before_action :load_stripe_plan
5
+ before_action :load_stripe_configuration
6
+ before_action :ensure_stripe_plan_exist
7
+ before_action :ensure_stripe_customer_exist
8
+ before_action :load_stripe_subscription, only: [:update, :destroy, :change_payment_details, :update_payment_details]
9
+ before_action :ensure_active_subscription, only: :downgrade
10
+
11
+ def create
12
+ checkout_session = Stripe::Checkout::Session.create(
13
+ mode: 'subscription',
14
+ customer: @stripe_customer.id,
15
+ customer_update: { address: 'auto', name: 'auto' },
16
+ client_reference_id: spree_current_user.id,
17
+ line_items: [
18
+ {
19
+ 'price': @stripe_plan.id,
20
+ 'quantity': 1
21
+ }
22
+ ],
23
+ automatic_tax: { enabled: @configuration.automatic_tax },
24
+ tax_id_collection: { enabled: @configuration.tax_id_collection },
25
+ billing_address_collection: @configuration.billing_address_collection,
26
+ success_url: stripe_plans_url,
27
+ cancel_url: stripe_plans_url
28
+ )
29
+ redirect_to checkout_session.url
30
+ rescue StandardError => e
31
+ Rails.logger.error e.message
32
+ flash[:error] = e.message
33
+ redirect_to stripe_plans_path
34
+ end
35
+
36
+ def update
37
+ @subscription.cancel_renewal
38
+ flash[:success] = I18n.t('spree_stripe_subscriptions.messages.success.successfully_canceled_renewal')
39
+ redirect_to stripe_plans_path
40
+ end
41
+
42
+ def change_payment_details
43
+ checkout_session = Stripe::Checkout::Session.create(
44
+ payment_method_types: ['card'],
45
+ mode: 'setup',
46
+ customer: @stripe_customer.id,
47
+ setup_intent_data: {
48
+ metadata: {
49
+ subscription_id: @subscription.stripe_subscription_id
50
+ }
51
+ },
52
+ success_url: "#{update_payment_details_stripe_plan_stripe_subscription_url(@plan, @subscription)}?session_id={CHECKOUT_SESSION_ID}",
53
+ cancel_url: stripe_plans_url
54
+ )
55
+ redirect_to checkout_session.url
56
+ rescue StandardError => e
57
+ Rails.logger.error e.message
58
+ flash[:error] = e.message
59
+ redirect_to stripe_plans_path
60
+ end
61
+
62
+ def update_payment_details
63
+ return unless (session_id = params[:session_id])
64
+
65
+ checkout_session = Stripe::Checkout::Session.retrieve(session_id)
66
+ setup_intent = Stripe::SetupIntent.retrieve(checkout_session.setup_intent)
67
+
68
+ # Set invoice_settings.default_payment_method on the Customer
69
+ Stripe::Customer.update(
70
+ @stripe_customer.id,
71
+ { invoice_settings: { default_payment_method: setup_intent.payment_method } }
72
+ )
73
+
74
+ # Set default_payment_method on the Subscription
75
+ Stripe::Subscription.update(
76
+ @subscription.stripe_subscription_id,
77
+ {
78
+ default_payment_method: setup_intent.payment_method
79
+ }
80
+ )
81
+
82
+ flash[:success] = I18n.t('spree_stripe_subscriptions.messages.success.successfully_updated')
83
+ redirect_to stripe_plans_path
84
+ rescue StandardError => e
85
+ Rails.logger.error e.message
86
+ flash[:error] = e.message
87
+ redirect_to stripe_plans_path
88
+ end
89
+
90
+ def downgrade
91
+ active_subscription = spree_current_user.stripe_subscriptions.active.find(params[:id])
92
+ destination_plan = Spree::StripePlan.active.find(params[:stripe_plan_id])
93
+
94
+ subscription_schedule = active_subscription.stripe_subscription_schedule
95
+
96
+ if subscription_schedule.present?
97
+ Stripe::SubscriptionSchedule.update(
98
+ subscription_schedule.id,
99
+ {
100
+ phases: [
101
+ {
102
+ items: [
103
+ { price: active_subscription.plan.stripe_plan_id, quantity: 1 }
104
+ ],
105
+ start_date: active_subscription.current_period_start.to_i,
106
+ end_date: active_subscription.current_period_end.to_i
107
+ },
108
+ {
109
+ items: [
110
+ { price: destination_plan.stripe_plan_id, quantity: 1 }
111
+ ],
112
+ start_date: active_subscription.current_period_end.to_i,
113
+ # end_date: active_subscription.current_period_end.to_i + 1.month.to_i
114
+ }
115
+ ]
116
+ }
117
+ )
118
+ flash[:success] = I18n.t('spree_stripe_subscriptions.messages.success.successfully_downgraded')
119
+ redirect_to stripe_plans_path
120
+ else
121
+ flash[:alert] = I18n.t('spree_stripe_subscriptions.messages.errors.cannot_process_request')
122
+ redirect_to stripe_plans_path
123
+ end
124
+ end
125
+
126
+ def destroy
127
+ @subscription.unsubscribe
128
+ flash[:success] = I18n.t('spree_stripe_subscriptions.messages.success.successfully_unsubscribed')
129
+ redirect_to stripe_plans_path
130
+ end
131
+
132
+ private
133
+
134
+ def login_required
135
+ raise CanCan::AccessDenied if spree_current_user.blank?
136
+ end
137
+
138
+ def load_stripe_plan
139
+ @plan = Spree::StripePlan.active.find(params[:stripe_plan_id])
140
+ end
141
+
142
+ def load_stripe_configuration
143
+ @configuration = @plan.configuration
144
+ end
145
+
146
+ def ensure_active_subscription
147
+ return if spree_current_user.stripe_subscriptions.active.exists?
148
+
149
+ flash[:alert] = I18n.t('spree_stripe_subscriptions.messages.errors.no_active_subscription')
150
+ redirect_to stripe_plans_path
151
+ end
152
+
153
+ def ensure_stripe_customer_exist
154
+ @stripe_customer = spree_current_user.find_or_create_stripe_customer
155
+ end
156
+
157
+ def ensure_stripe_plan_exist
158
+ @stripe_plan = @plan.find_or_create_stripe_plan
159
+ raise CanCan::AccessDenied if @stripe_plan.nil?
160
+ end
161
+
162
+ def load_stripe_subscription
163
+ @subscription = @plan.stripe_subscriptions.find(params[:id])
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,56 @@
1
+ # Ref: https://github.com/stripe-samples/checkout-foundations-ruby/blob/main/server.rb
2
+
3
+ module Spree
4
+ class StripeWebhooksController < BaseController
5
+ skip_before_action :verify_authenticity_token
6
+
7
+ before_action :load_stripe_configuration
8
+
9
+ respond_to :json
10
+
11
+ def handler
12
+ sig_header = request.env['HTTP_STRIPE_SIGNATURE']
13
+ payload = request.body.read
14
+
15
+ event = Stripe::Webhook.construct_event(
16
+ payload, sig_header, @stripe_configuration.preferred_webhook_secret
17
+ )
18
+
19
+ if event.present? && (%w[customer.subscription.updated customer.subscription.deleted].include? event.type)
20
+ subscription = Spree::StripeSubscription.create_or_update_subscription(event)
21
+ subscription.register_webhook_event(event)
22
+ elsif event.present? && (%w[subscription_schedule.updated].include? event.type)
23
+ subscription = Spree::StripeSubscription.update_subscription_schedule(event)
24
+ subscription.register_webhook_event(event) if subscription.present?
25
+ elsif event.present? && (%w[invoice.paid].include? event.type)
26
+ subscription = Spree::StripeSubscription.create_or_update_invoice(event)
27
+ subscription.register_webhook_event(event) if subscription.present?
28
+ else
29
+ Rails.logger.warn "Unhandled event type: #{event&.type}"
30
+ end
31
+
32
+ render json: { success: true }, status: :ok
33
+ rescue JSON::ParserError => e
34
+ # Invalid payload
35
+ Rails.logger.error e
36
+ render json: { success: false }, status: :not_found
37
+ rescue Stripe::SignatureVerificationError => e
38
+ # invalid signature
39
+ Rails.logger.error e
40
+ render json: { success: false }, status: :not_found
41
+ rescue ActiveRecord::RecordNotFound => e
42
+ # Either StripeCustomer or StripePlan doesn't exist in our records
43
+ Rails.logger.error e
44
+ render json: { success: false }, status: :ok
45
+ end
46
+
47
+ private
48
+
49
+ def load_stripe_configuration
50
+ @stripe_configuration = Spree::StripeConfiguration.active.last
51
+ return if @stripe_configuration.present? && @stripe_configuration.preferred_keys_available?
52
+
53
+ render json: { success: false }, status: :not_found
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,28 @@
1
+ module Spree
2
+ class StripeConfiguration < Spree::Base
3
+ acts_as_paranoid
4
+
5
+ preference :secret_key, :string
6
+ preference :public_key, :string
7
+ preference :webhook_secret, :string
8
+
9
+ ADDRESS_COLLECTION_OPTIONS = {
10
+ 'auto' => 'auto',
11
+ 'required' => 'required'
12
+ }.freeze
13
+
14
+ validates :billing_address_collection, inclusion: { in: ADDRESS_COLLECTION_OPTIONS.keys }
15
+
16
+ has_many :stripe_plans,
17
+ class_name: 'Spree::StripePlan',
18
+ foreign_key: :configuration_id,
19
+ inverse_of: :configuration,
20
+ dependent: :restrict_with_error
21
+
22
+ scope :active, -> { where(active: true) }
23
+
24
+ def preferred_keys_available?
25
+ preferred_secret_key.present? && preferred_public_key.present? && preferred_webhook_secret.present?
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,30 @@
1
+ module Spree
2
+ class StripeCustomer < Spree::Base
3
+ acts_as_paranoid
4
+
5
+ belongs_to :user, class_name: Spree.user_class.to_s
6
+
7
+ has_many :stripe_subscriptions,
8
+ class_name: 'Spree::StripeSubscription',
9
+ foreign_key: :customer_id,
10
+ inverse_of: :customer,
11
+ dependent: :restrict_with_error
12
+ has_many :stripe_invoices,
13
+ class_name: 'Spree::StripeInvoice',
14
+ foreign_key: :customer_id,
15
+ inverse_of: :customer,
16
+ dependent: :restrict_with_error
17
+
18
+ def stripe_customer
19
+ stripe_customer = nil
20
+ return nil if deleted_at.present?
21
+
22
+ stripe_customer = Stripe::Customer.retrieve(stripe_customer_id)
23
+ rescue StandardError
24
+ destroy
25
+ stripe_customer = nil
26
+ ensure
27
+ stripe_customer
28
+ end
29
+ end
30
+ end