saas_payments 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +146 -0
- data/Rakefile +32 -0
- data/app/assets/config/saas_payments_manifest.js +2 -0
- data/app/assets/javascripts/saas_payments/application.js +15 -0
- data/app/assets/javascripts/saas_payments/elements.js +146 -0
- data/app/assets/stylesheets/saas_payments/application.css +15 -0
- data/app/assets/stylesheets/saas_payments/elements.css +31 -0
- data/app/controllers/concerns/saas_payments/subscription_concerns.rb +173 -0
- data/app/controllers/concerns/saas_payments/webhook_concerns.rb +22 -0
- data/app/controllers/saas_payments/application_controller.rb +17 -0
- data/app/controllers/saas_payments/webhook_controller.rb +10 -0
- data/app/helpers/saas_payments/application_helper.rb +4 -0
- data/app/jobs/saas_payments/application_job.rb +4 -0
- data/app/lib/saas_payments/products_service.rb +25 -0
- data/app/lib/saas_payments/webhook/customer_event.rb +29 -0
- data/app/lib/saas_payments/webhook/plan_event.rb +25 -0
- data/app/lib/saas_payments/webhook/product_event.rb +25 -0
- data/app/lib/saas_payments/webhook/session_event.rb +6 -0
- data/app/lib/saas_payments/webhook/subscription_event.rb +25 -0
- data/app/lib/saas_payments/webhook_service.rb +39 -0
- data/app/mailers/saas_payments/application_mailer.rb +6 -0
- data/app/models/concerns/saas_payments/stripe_model.rb +32 -0
- data/app/models/saas_payments/application_record.rb +5 -0
- data/app/models/saas_payments/customer.rb +42 -0
- data/app/models/saas_payments/plan.rb +28 -0
- data/app/models/saas_payments/product.rb +24 -0
- data/app/models/saas_payments/subscription.rb +31 -0
- data/app/views/saas_payments/_scripts.html.erb +5 -0
- data/app/views/saas_payments/_stripe_elements.html.erb +26 -0
- data/config/initializers/stripe.rb +0 -0
- data/config/routes.rb +4 -0
- data/db/migrate/20190818184232_create_saas_payments_subscriptions.rb +28 -0
- data/db/migrate/20190820040835_create_saas_payments_products.rb +19 -0
- data/db/migrate/20190820040959_create_saas_payments_plans.rb +25 -0
- data/db/migrate/20190823023237_create_saas_payments_customers.rb +19 -0
- data/lib/saas_payments.rb +15 -0
- data/lib/saas_payments/config.rb +9 -0
- data/lib/saas_payments/engine.rb +18 -0
- data/lib/saas_payments/errors.rb +3 -0
- data/lib/saas_payments/version.rb +3 -0
- data/lib/tasks/products.rake +6 -0
- data/lib/tasks/saas_payments_tasks.rake +4 -0
- metadata +184 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 399ad7181e74c3e135ffbf7c1990babb8d1aec389a453fe0b5b2414401bb6f9b
|
4
|
+
data.tar.gz: da5bc44556fd4496ae64bece2da30f42cbdba80f77d79278714dad01681075f2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: da830a93f1a7c9a757fa2a21c2068296b6d40198479ff802d8740a09ff0ee297582adbc94f50b127ee315ad3e3e00d5e08efb6818bba62753ff24642af20e4f5
|
7
|
+
data.tar.gz: 0765fa876818e55cd8b521199edf1adc58a77b96269bb54012a5ab1f74bebbab03555ac3f4571d98e02d8ad8b2479d82f79a9ad571157227944b79afd911facb
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2019 Cody
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
# SaasPayments
|
2
|
+
|
3
|
+
SaasPayments is a thin wrapper around the Stripe API to manage the most common
|
4
|
+
SAAS subscription actions. This gem includes
|
5
|
+
|
6
|
+
- [ ] Subscription model to hold the state of a user's subscription
|
7
|
+
- [ ] Payment form template
|
8
|
+
- [ ] Webhook to handle subscription updates from Stripe
|
9
|
+
- [ ] Sign up, cancel, and change plan routes
|
10
|
+
|
11
|
+
|
12
|
+
This Gem also assumes you already have:
|
13
|
+
|
14
|
+
- A `User` model (If you don't, try [Devise](https://github.com/plataformatec/devise))
|
15
|
+
|
16
|
+
# Getting Started
|
17
|
+
|
18
|
+
## Install
|
19
|
+
Add this line to your application's Gemfile:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
# Gemfile
|
23
|
+
|
24
|
+
gem 'saas_payments'
|
25
|
+
```
|
26
|
+
|
27
|
+
And then run:
|
28
|
+
```bash
|
29
|
+
bundle install
|
30
|
+
```
|
31
|
+
|
32
|
+
## Migrations
|
33
|
+
```bash
|
34
|
+
rake saas_payments:install:migrations
|
35
|
+
rails db:migrate
|
36
|
+
```
|
37
|
+
|
38
|
+
## Configuration
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
SaasPayments.configure do |config|
|
42
|
+
# Stripe API Keys
|
43
|
+
config.stripe_secret_key = "sk_...."
|
44
|
+
config.stripe_publishable_key = "pk_..."
|
45
|
+
|
46
|
+
# Path to return to after successful subscription changes
|
47
|
+
config.account_path = '/dashboard'
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
## Importing data from Stripe
|
52
|
+
|
53
|
+
### Rake task to import Products/Plans
|
54
|
+
```bash
|
55
|
+
rake products:sync
|
56
|
+
```
|
57
|
+
|
58
|
+
## Managing Subscriptions
|
59
|
+
|
60
|
+
Subscriptions are managed using a set of controller concerns. Subscriptions
|
61
|
+
can be created and modified by including the `SaasPayments::Subscriptions`
|
62
|
+
concern into your controller.
|
63
|
+
|
64
|
+
### Creating subscriptions
|
65
|
+
|
66
|
+
Currently, only one subscription is supported per user. Should a user enter
|
67
|
+
their card information more than once, their current subscription will be
|
68
|
+
updated, rather than creating a second subscription.
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
class AccountsController < ApplicationController
|
72
|
+
# Include the subscriptions concern
|
73
|
+
include SaasPayments::Subscriptions
|
74
|
+
|
75
|
+
def create
|
76
|
+
sign_up_for_plan @current_user, @plan
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
```
|
81
|
+
|
82
|
+
### Cancellation
|
83
|
+
|
84
|
+
NOTE: Canceled subscriptions will be canceled at the end of the period.
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
# Host application
|
88
|
+
class AccountsController < ApplicationController
|
89
|
+
# Include the subscriptions concern
|
90
|
+
include SaasPayments::Subscriptions
|
91
|
+
|
92
|
+
def cancel
|
93
|
+
# Perform any logic you need
|
94
|
+
|
95
|
+
# Call the cancel_subscription method to perform the
|
96
|
+
# cancellation
|
97
|
+
cancel_at_period_end @current_user
|
98
|
+
end
|
99
|
+
end
|
100
|
+
```
|
101
|
+
|
102
|
+
### Changing Plans
|
103
|
+
|
104
|
+
The subscriptions concern also contains a method for changing plans.
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
# Host application
|
108
|
+
class AccountsController < ApplicationController
|
109
|
+
# Include the subscriptions concern
|
110
|
+
include SaasPayments::Subscriptions
|
111
|
+
|
112
|
+
def change_plan
|
113
|
+
# Perform any logic you need
|
114
|
+
|
115
|
+
# Call the change_plan method to change the plan a user is
|
116
|
+
# subscribed to.
|
117
|
+
change_plan @current_user, @plan
|
118
|
+
end
|
119
|
+
end
|
120
|
+
```
|
121
|
+
|
122
|
+
## Development
|
123
|
+
|
124
|
+
- Use `bin/rails`. For example, generators using this executable will create
|
125
|
+
models/controllers/etc that are namespaced to the saas_payments gem
|
126
|
+
|
127
|
+
|
128
|
+
## Testing ([Rails Guide](https://guides.rubyonrails.org/engines.html#testing-an-engine))
|
129
|
+
```bash
|
130
|
+
make test
|
131
|
+
```
|
132
|
+
|
133
|
+
## Contributing
|
134
|
+
Contribution directions go here.
|
135
|
+
|
136
|
+
|
137
|
+
## License
|
138
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
139
|
+
|
140
|
+
|
141
|
+
## TODO
|
142
|
+
|
143
|
+
- [ ] Per unit pricing
|
144
|
+
- [ ] Tiered pricing
|
145
|
+
- [ ] Metered pricing
|
146
|
+
- [ ] Product pricing
|
data/Rakefile
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'SaasPayments'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
|
18
|
+
load 'rails/tasks/engine.rake'
|
19
|
+
|
20
|
+
load 'rails/tasks/statistics.rake'
|
21
|
+
|
22
|
+
require 'bundler/gem_tasks'
|
23
|
+
|
24
|
+
require 'rake/testtask'
|
25
|
+
|
26
|
+
Rake::TestTask.new(:test) do |t|
|
27
|
+
t.libs << 'test'
|
28
|
+
t.pattern = 'test/**/*_test.rb'
|
29
|
+
t.verbose = false
|
30
|
+
end
|
31
|
+
|
32
|
+
task default: :test
|
@@ -0,0 +1,15 @@
|
|
1
|
+
// This is a manifest file that'll be compiled into application.js, which will include all the files
|
2
|
+
// listed below.
|
3
|
+
//
|
4
|
+
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
|
5
|
+
// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
|
6
|
+
//
|
7
|
+
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
|
8
|
+
// compiled file. JavaScript code in this file should be added after the last require_* statement.
|
9
|
+
//
|
10
|
+
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
|
11
|
+
// about supported directives.
|
12
|
+
//
|
13
|
+
//= require rails-ujs
|
14
|
+
//= require activestorage
|
15
|
+
//= require_tree .
|
@@ -0,0 +1,146 @@
|
|
1
|
+
'use strict';
|
2
|
+
|
3
|
+
document.addEventListener('DOMContentLoaded', function() {
|
4
|
+
|
5
|
+
var Card = function(stripe) {
|
6
|
+
// Create an instance of Elements.
|
7
|
+
var elements = stripe.elements();
|
8
|
+
|
9
|
+
// Custom styling can be passed to options when creating an Element.
|
10
|
+
// (Note that this demo uses a wider set of styles than the guide below.)
|
11
|
+
var style = {
|
12
|
+
base: {
|
13
|
+
color: '#32325d',
|
14
|
+
fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
|
15
|
+
fontSmoothing: 'antialiased',
|
16
|
+
fontSize: '16px',
|
17
|
+
'::placeholder': {
|
18
|
+
color: '#aab7c4'
|
19
|
+
}
|
20
|
+
},
|
21
|
+
invalid: {
|
22
|
+
color: '#fa755a',
|
23
|
+
iconColor: '#fa755a'
|
24
|
+
}
|
25
|
+
};
|
26
|
+
|
27
|
+
// Create an instance of the card Element.
|
28
|
+
var card = elements.create('card', {style: style});
|
29
|
+
|
30
|
+
// Handle real-time validation errors from the card Element.
|
31
|
+
card.addEventListener('change', function(event) {
|
32
|
+
var displayError = document.getElementById('card-errors');
|
33
|
+
if (event.error) {
|
34
|
+
displayError.textContent = event.error.message;
|
35
|
+
} else {
|
36
|
+
displayError.textContent = '';
|
37
|
+
}
|
38
|
+
});
|
39
|
+
|
40
|
+
return card
|
41
|
+
}
|
42
|
+
|
43
|
+
var SPElements = function() {
|
44
|
+
// Retrieve the publishable key
|
45
|
+
var key = document.getElementById("publishable-key").dataset.publishable
|
46
|
+
|
47
|
+
// Create a Stripe client.
|
48
|
+
var stripe = Stripe(key);
|
49
|
+
|
50
|
+
var options = options || {}
|
51
|
+
var card = Card(stripe);
|
52
|
+
var forms = document.getElementsByClassName('payment-form')
|
53
|
+
|
54
|
+
var init = function() {
|
55
|
+
for (var i = 0; i < forms.length; i++) {
|
56
|
+
initForm(forms[i])
|
57
|
+
}
|
58
|
+
if (forms.length === 1) {
|
59
|
+
this.show(forms[0].id)
|
60
|
+
}
|
61
|
+
}
|
62
|
+
|
63
|
+
// Submit the form with the token ID.
|
64
|
+
var stripeTokenHandler = function(form, token) {
|
65
|
+
// Insert the token ID into the form so it gets submitted to the server
|
66
|
+
var hiddenInput = document.createElement('input');
|
67
|
+
hiddenInput.setAttribute('type', 'hidden');
|
68
|
+
hiddenInput.setAttribute('name', 'stripeToken');
|
69
|
+
hiddenInput.setAttribute('value', token.id);
|
70
|
+
form.appendChild(hiddenInput);
|
71
|
+
|
72
|
+
// Submit the form
|
73
|
+
form.submit();
|
74
|
+
}
|
75
|
+
|
76
|
+
var initForm = function(form) {
|
77
|
+
// Handle form submission.
|
78
|
+
form.addEventListener('submit', onSubmit.bind(this, form))
|
79
|
+
}
|
80
|
+
|
81
|
+
var onSubmit = function(form, event) {
|
82
|
+
if (event) {
|
83
|
+
event.preventDefault();
|
84
|
+
}
|
85
|
+
|
86
|
+
stripe.createToken(card).then(function(result) {
|
87
|
+
if (result.error) {
|
88
|
+
// Inform the user if there was an error.
|
89
|
+
var errorElement = document.getElementById('card-errors');
|
90
|
+
errorElement.textContent = result.error.message;
|
91
|
+
} else {
|
92
|
+
// Send the token to your server.
|
93
|
+
stripeTokenHandler(form, result.token);
|
94
|
+
}
|
95
|
+
});
|
96
|
+
}
|
97
|
+
|
98
|
+
var hideAll = function() {
|
99
|
+
var forms = document.getElementsByClassName('payment-form')
|
100
|
+
for (var i = 0; i < forms.length; i++) {
|
101
|
+
forms[i].style.display = 'none'
|
102
|
+
}
|
103
|
+
}
|
104
|
+
|
105
|
+
var setHiddenValue = function(form, className, value) {
|
106
|
+
var field = form.getElementsByClassName(className)[0]
|
107
|
+
field.value = value
|
108
|
+
}
|
109
|
+
|
110
|
+
this.show = function(id) {
|
111
|
+
this.form = document.getElementById(id)
|
112
|
+
|
113
|
+
if(!this.form) {
|
114
|
+
console.error("Could not find form with id ("+id+")")
|
115
|
+
return
|
116
|
+
}
|
117
|
+
|
118
|
+
hideAll()
|
119
|
+
this.form.style.display = ''
|
120
|
+
card.unmount()
|
121
|
+
card.mount('#' + id + "_card") // Add an instance of the card Element into the `card-element` <div>.
|
122
|
+
}
|
123
|
+
|
124
|
+
this.setData = function(options) {
|
125
|
+
if(!this.form) {
|
126
|
+
console.error("Form must be shown first")
|
127
|
+
return
|
128
|
+
}
|
129
|
+
|
130
|
+
var defaults = ['plan_id', 'user_id']
|
131
|
+
for (var i = 0; i < defaults.length; i++) {
|
132
|
+
var key = defaults[i]
|
133
|
+
if(options[key]) setHiddenValue(this.form, 'form_'+key, options[key])
|
134
|
+
}
|
135
|
+
}
|
136
|
+
|
137
|
+
this.submit = function() {
|
138
|
+
onSubmit(this.form)
|
139
|
+
}
|
140
|
+
|
141
|
+
init.call(this)
|
142
|
+
}
|
143
|
+
|
144
|
+
window.SPElements = SPElements
|
145
|
+
})
|
146
|
+
|
@@ -0,0 +1,15 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
11
|
+
* It is generally better to create a new file per style scope.
|
12
|
+
*
|
13
|
+
*= require_tree .
|
14
|
+
*= require_self
|
15
|
+
*/
|
@@ -0,0 +1,31 @@
|
|
1
|
+
/**
|
2
|
+
* The CSS shown here will not be introduced in the Quickstart guide, but shows
|
3
|
+
* how you can use CSS to style your Element's container.
|
4
|
+
*/
|
5
|
+
.StripeElement {
|
6
|
+
box-sizing: border-box;
|
7
|
+
|
8
|
+
height: 40px;
|
9
|
+
|
10
|
+
padding: 10px 12px;
|
11
|
+
|
12
|
+
border: 1px solid transparent;
|
13
|
+
border-radius: 4px;
|
14
|
+
background-color: white;
|
15
|
+
|
16
|
+
box-shadow: 0 1px 3px 0 #e6ebf1;
|
17
|
+
-webkit-transition: box-shadow 150ms ease;
|
18
|
+
transition: box-shadow 150ms ease;
|
19
|
+
}
|
20
|
+
|
21
|
+
.StripeElement--focus {
|
22
|
+
box-shadow: 0 1px 3px 0 #cfd7df;
|
23
|
+
}
|
24
|
+
|
25
|
+
.StripeElement--invalid {
|
26
|
+
border-color: #fa755a;
|
27
|
+
}
|
28
|
+
|
29
|
+
.StripeElement--webkit-autofill {
|
30
|
+
background-color: #fefde5 !important;
|
31
|
+
}
|
@@ -0,0 +1,173 @@
|
|
1
|
+
module SaasPayments
|
2
|
+
module SubscriptionConcerns
|
3
|
+
Stripe.api_key = SaasPayments.config.stripe_secret_key
|
4
|
+
|
5
|
+
class FailedPayment < StandardError; end
|
6
|
+
class ActionRequired < StandardError; end
|
7
|
+
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
def sign_up_for_plan user, plan
|
11
|
+
customer = get_customer user
|
12
|
+
sub = update_subscription customer, plan
|
13
|
+
render_success
|
14
|
+
rescue ActionRequired
|
15
|
+
render_action_required
|
16
|
+
rescue FailedPayment
|
17
|
+
render_card_failure
|
18
|
+
rescue Stripe::CardError
|
19
|
+
render_card_failure
|
20
|
+
end
|
21
|
+
|
22
|
+
def change_plan user, plan
|
23
|
+
customer = get_customer user
|
24
|
+
sub = customer.subscription(remote: true)
|
25
|
+
update_plan sub, customer, plan
|
26
|
+
render_success
|
27
|
+
rescue StandardError
|
28
|
+
render_error
|
29
|
+
end
|
30
|
+
|
31
|
+
def cancel_at_period_end user
|
32
|
+
customer = get_customer user
|
33
|
+
sub = customer.subscription(remote: true)
|
34
|
+
|
35
|
+
# Customer has an existing subscription, change plans
|
36
|
+
remote_sub = Stripe::Subscription.update(sub[:id], {
|
37
|
+
cancel_at_period_end: true
|
38
|
+
})
|
39
|
+
|
40
|
+
customer.subscription.update(Subscription.from_stripe(remote_sub))
|
41
|
+
render_success
|
42
|
+
rescue StandardError
|
43
|
+
render_error
|
44
|
+
end
|
45
|
+
|
46
|
+
def resume_subscription user
|
47
|
+
customer = get_customer user
|
48
|
+
sub = customer.subscription
|
49
|
+
remote_sub = Stripe::Subscription.update(sub.stripe_id, {
|
50
|
+
cancel_at_period_end: false
|
51
|
+
})
|
52
|
+
|
53
|
+
customer.subscription.update(Subscription.from_stripe(remote_sub))
|
54
|
+
render_success
|
55
|
+
rescue StandardError
|
56
|
+
render_error
|
57
|
+
end
|
58
|
+
|
59
|
+
def cancel_now user
|
60
|
+
customer = get_customer user
|
61
|
+
Stripe::Subscription.delete(customer.subscription.stripe_id)
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def render_success
|
68
|
+
respond_to do |format|
|
69
|
+
format.html { redirect_to SaasPayments.config.account_path }
|
70
|
+
format.json { render json: { message: "success" } }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def render_error
|
75
|
+
format.html {
|
76
|
+
flash[:sp_error] = 'Failed to update plan'
|
77
|
+
redirect_back fallback_location: root_path
|
78
|
+
}
|
79
|
+
format.json { render json: { message: "update_failed" }, status: :bad_request }
|
80
|
+
end
|
81
|
+
|
82
|
+
def render_action_required
|
83
|
+
respond_to do |format|
|
84
|
+
format.html {
|
85
|
+
flash[:sp_notice] = 'Payment requires customer action'
|
86
|
+
redirect_to SaasPayments.successPath
|
87
|
+
}
|
88
|
+
format.json { render json: { message: "action_required" } }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def render_card_failure
|
93
|
+
respond_to do |format|
|
94
|
+
format.html {
|
95
|
+
flash[:sp_error] = 'Failed to process card'
|
96
|
+
redirect_back fallback_location: root_path
|
97
|
+
}
|
98
|
+
format.json { render json: { message: "card_error" }, status: :bad_request }
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def get_customer user
|
103
|
+
# Try to find a customer in the database by the user id
|
104
|
+
customer = SaasPayments::Customer.find_by_user_id(user.id)
|
105
|
+
|
106
|
+
# If no customer exists, create a new one
|
107
|
+
if !customer.present?
|
108
|
+
# Create a new customer in stripe
|
109
|
+
c = Stripe::Customer.create({
|
110
|
+
email: user.email,
|
111
|
+
source: params.require(:stripeToken),
|
112
|
+
metadata: { user_id: user.id }
|
113
|
+
})
|
114
|
+
|
115
|
+
# Create a new customer locally that maps to the stripe customer
|
116
|
+
customer = Customer.create(Customer.from_stripe(c).merge({ user_id: user.id }))
|
117
|
+
end
|
118
|
+
customer
|
119
|
+
end
|
120
|
+
|
121
|
+
def update_subscription customer, plan
|
122
|
+
sub = customer.subscription(remote: true)
|
123
|
+
if sub.present?
|
124
|
+
update_plan sub, customer, plan
|
125
|
+
else
|
126
|
+
sub = create_subscription customer, plan
|
127
|
+
end
|
128
|
+
|
129
|
+
sub
|
130
|
+
end
|
131
|
+
|
132
|
+
def update_plan sub, customer, plan
|
133
|
+
# Customer has an existing subscription, change plans
|
134
|
+
remote_sub = Stripe::Subscription.update(sub[:id], {
|
135
|
+
cancel_at_period_end: false,
|
136
|
+
items: [{
|
137
|
+
id: sub[:items][:data][0][:id],
|
138
|
+
plan: plan.stripe_id
|
139
|
+
}]
|
140
|
+
})
|
141
|
+
|
142
|
+
customer.subscription.update(Subscription.from_stripe(remote_sub))
|
143
|
+
end
|
144
|
+
|
145
|
+
def create_subscription customer, plan
|
146
|
+
# Customer does not have a subscription, sign them up
|
147
|
+
sub = Stripe::Subscription.create({
|
148
|
+
customer: customer.stripe_id,
|
149
|
+
items: [{ plan: plan.stripe_id }]
|
150
|
+
})
|
151
|
+
complete_subscription sub
|
152
|
+
end
|
153
|
+
|
154
|
+
def complete_subscription sub
|
155
|
+
if incomplete?(sub)
|
156
|
+
raise FailedPayment if payment_intent(sub, :requires_payment_method)
|
157
|
+
raise ActionRequired if payment_intent(sub, :requires_action)
|
158
|
+
end
|
159
|
+
|
160
|
+
# Successful payment, or trialling: provision the subscription
|
161
|
+
# Create a local subscription
|
162
|
+
Subscription.create(Subscription.from_stripe(sub))
|
163
|
+
end
|
164
|
+
|
165
|
+
def incomplete? sub
|
166
|
+
sub[:status] == "incomplete"
|
167
|
+
end
|
168
|
+
|
169
|
+
def payment_intent sub, intent
|
170
|
+
sub[:latest_invoice][:payment_intent][:status] == intent.to_s
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|