saas_payments 0.0.2
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.
- 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
@@ -0,0 +1,22 @@
|
|
1
|
+
module SaasPayments
|
2
|
+
module WebhookConcerns
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
Stripe.api_key = SaasPayments.config.stripe_secret_key
|
5
|
+
SECRET = SaasPayments.config.webhook_secret
|
6
|
+
|
7
|
+
def webhook
|
8
|
+
event = params
|
9
|
+
if SaasPayments.config.webhook_secret.present?
|
10
|
+
header = request.headers['HTTP_STRIPE_SIGNATURE']
|
11
|
+
event = Stripe::Webhook.construct_event(request.body.read, header, SECRET)
|
12
|
+
end
|
13
|
+
WebhookService.new(event).process
|
14
|
+
head :ok
|
15
|
+
rescue ActiveRecord::RecordNotFound
|
16
|
+
head :not_found
|
17
|
+
rescue StandardError => e
|
18
|
+
head :bad_request
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module SaasPayments
|
2
|
+
class ApplicationController < ActionController::Base
|
3
|
+
protect_from_forgery with: :exception
|
4
|
+
before_action :init_stripe
|
5
|
+
|
6
|
+
def index
|
7
|
+
head :ok
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def init_stripe
|
13
|
+
Stripe.api_key = SaasPayments.config.stripe_secret_key || ENV["STRIPE_SECRET_KEY"]
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module SaasPayments
|
2
|
+
class ProductsService
|
3
|
+
def self.sync
|
4
|
+
Stripe.api_key = SaasPayments.config.stripe_secret_key
|
5
|
+
Stripe::Product.all.each{ |p| create_product p }
|
6
|
+
Stripe::Plan.list.each{ |p| create_plan p }
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def self.create_product product
|
12
|
+
Product.create!(Product.from_stripe(product))
|
13
|
+
rescue ActiveRecord::RecordInvalid
|
14
|
+
puts "Product (#{product[:id]}) already exists. Updating..."
|
15
|
+
Product.find_by_stripe_id(product[:id]).update!(Product.from_stripe(product))
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.create_plan plan
|
19
|
+
Plan.create!(Plan.from_stripe(plan))
|
20
|
+
rescue ActiveRecord::RecordInvalid
|
21
|
+
puts "Plan (#{plan[:id]}) already exists. Updating..."
|
22
|
+
Plan.find_by_stripe_id(plan[:id]).update!(Plan.from_stripe(plan))
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module SaasPayments
|
2
|
+
class Webhook::CustomerEvent
|
3
|
+
def created data
|
4
|
+
Customer.create(Customer.from_stripe(data).merge({ user_id: user_id(data) }))
|
5
|
+
end
|
6
|
+
|
7
|
+
def updated data
|
8
|
+
customer = Customer.find_by_stripe_id(data[:id])
|
9
|
+
raise_not_found(data[:id]) unless customer
|
10
|
+
customer.update(Customer.from_stripe(data))
|
11
|
+
end
|
12
|
+
|
13
|
+
def deleted data
|
14
|
+
customer = Customer.find_by_stripe_id(data[:id])
|
15
|
+
raise_not_found(data[:id]) unless customer
|
16
|
+
customer.delete
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def user_id data
|
22
|
+
data[:metadata][:user_id] rescue nil
|
23
|
+
end
|
24
|
+
|
25
|
+
def raise_not_found id
|
26
|
+
raise ActiveRecord::RecordNotFound.new("Could not find customer with stripe id: #{id}")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module SaasPayments
|
2
|
+
class Webhook::PlanEvent
|
3
|
+
def created data
|
4
|
+
Plan.create!(Plan.from_stripe(data))
|
5
|
+
end
|
6
|
+
|
7
|
+
def updated data
|
8
|
+
plan = Plan.find_by_stripe_id(data[:id])
|
9
|
+
raise_not_found(data[:id]) unless plan
|
10
|
+
plan.update(Plan.from_stripe(data))
|
11
|
+
end
|
12
|
+
|
13
|
+
def deleted data
|
14
|
+
plan = Plan.find_by_stripe_id(data[:id])
|
15
|
+
raise_not_found(data[:id]) unless plan
|
16
|
+
plan.delete
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def raise_not_found id
|
22
|
+
raise ActiveRecord::RecordNotFound.new("Could not find plan with stripe id: #{id}")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module SaasPayments
|
2
|
+
class Webhook::ProductEvent
|
3
|
+
def created data
|
4
|
+
Product.create!(Product.from_stripe(data))
|
5
|
+
end
|
6
|
+
|
7
|
+
def updated data
|
8
|
+
product = Product.find_by_stripe_id(data[:id])
|
9
|
+
raise_not_found(data[:id]) unless product
|
10
|
+
product.update(Product.from_stripe(data))
|
11
|
+
end
|
12
|
+
|
13
|
+
def deleted data
|
14
|
+
product = Product.find_by_stripe_id(data[:id])
|
15
|
+
raise_not_found(data[:id]) unless product
|
16
|
+
product.delete
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def raise_not_found id
|
22
|
+
raise ActiveRecord::RecordNotFound.new("Could not find product with stripe_id: #{id}")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module SaasPayments
|
2
|
+
class Webhook::SubscriptionEvent
|
3
|
+
def created data
|
4
|
+
Subscription.create!(Subscription.from_stripe(data))
|
5
|
+
end
|
6
|
+
|
7
|
+
def updated data
|
8
|
+
sub = Subscription.find_by_stripe_id(data[:id])
|
9
|
+
raise_not_found(data[:id]) unless sub
|
10
|
+
sub.update(Subscription.from_stripe(data))
|
11
|
+
end
|
12
|
+
|
13
|
+
def deleted data
|
14
|
+
sub = Subscription.find_by_stripe_id(data[:id])
|
15
|
+
raise_not_found(data[:id]) unless sub
|
16
|
+
sub.delete
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def raise_not_found id
|
22
|
+
raise ActiveRecord::RecordNotFound.new("Could not find subscription with stripe id: #{id}")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module SaasPayments
|
2
|
+
class WebhookService
|
3
|
+
EVENTS = {
|
4
|
+
# Register Product events
|
5
|
+
'product.created' => [Webhook::ProductEvent, 'created'],
|
6
|
+
'product.deleted' => [Webhook::ProductEvent, 'deleted'],
|
7
|
+
'product.updated' => [Webhook::ProductEvent, 'updated'],
|
8
|
+
|
9
|
+
# Register Plan events
|
10
|
+
'plan.created' => [Webhook::PlanEvent, 'created'],
|
11
|
+
'plan.deleted' => [Webhook::PlanEvent, 'deleted'],
|
12
|
+
'plan.updated' => [Webhook::PlanEvent, 'updated'],
|
13
|
+
|
14
|
+
# Register Customer events
|
15
|
+
'customer.created' => [Webhook::CustomerEvent, 'created'],
|
16
|
+
'customer.deleted' => [Webhook::CustomerEvent, 'deleted'],
|
17
|
+
'customer.updated' => [Webhook::CustomerEvent, 'updated'],
|
18
|
+
|
19
|
+
# Register Subscription events
|
20
|
+
'customer.subscription.created' => [Webhook::SubscriptionEvent, 'created'],
|
21
|
+
'customer.subscription.deleted' => [Webhook::SubscriptionEvent, 'deleted'],
|
22
|
+
'customer.subscription.updated' => [Webhook::SubscriptionEvent, 'updated'],
|
23
|
+
|
24
|
+
# Register Session events
|
25
|
+
'checkout.session.completed' => [Webhook::SessionEvent, 'completed']
|
26
|
+
}
|
27
|
+
|
28
|
+
def initialize event
|
29
|
+
@event = event
|
30
|
+
end
|
31
|
+
|
32
|
+
def process
|
33
|
+
event, action = EVENTS[@event[:type]]
|
34
|
+
raise WebhookError.new("Unrecognized event") unless event && action
|
35
|
+
event.new.send(action, @event[:data][:object])
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module SaasPayments
|
2
|
+
module StripeModel
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
def truthy? val
|
7
|
+
val.present? ? val : false
|
8
|
+
end
|
9
|
+
|
10
|
+
def stripe_hash data
|
11
|
+
(data || {}).to_hash
|
12
|
+
end
|
13
|
+
|
14
|
+
def date ts
|
15
|
+
ts.present? ? Time.at(ts).to_datetime : nil
|
16
|
+
rescue
|
17
|
+
nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def parse_id val
|
21
|
+
return nil unless val
|
22
|
+
|
23
|
+
if val.is_a?(String)
|
24
|
+
val
|
25
|
+
else
|
26
|
+
val[:id]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module SaasPayments
|
2
|
+
class Customer < ApplicationRecord
|
3
|
+
include StripeModel
|
4
|
+
|
5
|
+
validates_uniqueness_of :stripe_id
|
6
|
+
|
7
|
+
belongs_to :user, optional: true
|
8
|
+
|
9
|
+
serialize :discount
|
10
|
+
serialize :metadata
|
11
|
+
|
12
|
+
def self.from_stripe c
|
13
|
+
{
|
14
|
+
stripe_id: c[:id],
|
15
|
+
delinquent: truthy?(c[:delinquent]),
|
16
|
+
description: c[:description],
|
17
|
+
discount: stripe_hash(c[:discount]),
|
18
|
+
email: c[:email],
|
19
|
+
livemode: truthy?(c[:livemode]),
|
20
|
+
metadata: stripe_hash(c[:metadata]),
|
21
|
+
name: c[:name]
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def subscription options={}
|
26
|
+
sub = Subscription.where(customer_id: stripe_id).first
|
27
|
+
if sub.present? && options[:remote]
|
28
|
+
sub = Stripe::Subscription.retrieve(sub.stripe_id)
|
29
|
+
end
|
30
|
+
sub
|
31
|
+
rescue Stripe::InvalidRequestError => e
|
32
|
+
raise e unless /no such/i =~ e.message
|
33
|
+
nil
|
34
|
+
end
|
35
|
+
|
36
|
+
def plan
|
37
|
+
return nil if !subscription.present?
|
38
|
+
Plan.find_by_stripe_id(subscription.plan_id)
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module SaasPayments
|
2
|
+
class Plan < ApplicationRecord
|
3
|
+
include StripeModel
|
4
|
+
|
5
|
+
validates_uniqueness_of :stripe_id
|
6
|
+
|
7
|
+
serialize :metadata
|
8
|
+
|
9
|
+
def self.from_stripe p
|
10
|
+
{
|
11
|
+
product_id: parse_id(p[:product]),
|
12
|
+
|
13
|
+
stripe_id: p[:id],
|
14
|
+
active: p[:active],
|
15
|
+
amount: p[:amount].to_i,
|
16
|
+
amount_decimal: p[:amount_decimal],
|
17
|
+
currency: p[:currency],
|
18
|
+
interval: p[:interval],
|
19
|
+
interval_count: p[:interval_count].to_i,
|
20
|
+
livemode: truthy?(p[:livemode]),
|
21
|
+
metadata: stripe_hash(p[:metadata]),
|
22
|
+
nickname: p[:nickname],
|
23
|
+
trial_period_days: p[:trial_period_days].to_i
|
24
|
+
}
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module SaasPayments
|
2
|
+
class Product < ApplicationRecord
|
3
|
+
include StripeModel
|
4
|
+
|
5
|
+
validates_uniqueness_of :stripe_id
|
6
|
+
|
7
|
+
has_many :plans
|
8
|
+
|
9
|
+
serialize :metadata
|
10
|
+
|
11
|
+
def self.from_stripe p
|
12
|
+
{
|
13
|
+
stripe_id: p[:id],
|
14
|
+
active: p[:active],
|
15
|
+
caption: p[:caption],
|
16
|
+
livemode: truthy?(p[:livemode]),
|
17
|
+
metadata: stripe_hash(p[:metadata]),
|
18
|
+
name: p[:name],
|
19
|
+
unit_label: p[:unit_label],
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module SaasPayments
|
2
|
+
class Subscription < ApplicationRecord
|
3
|
+
include StripeModel
|
4
|
+
|
5
|
+
validates_uniqueness_of :stripe_id
|
6
|
+
|
7
|
+
serialize :metadata
|
8
|
+
|
9
|
+
def self.from_stripe s
|
10
|
+
{
|
11
|
+
plan_id: parse_id(s[:plan]),
|
12
|
+
customer_id: parse_id(s[:customer]),
|
13
|
+
|
14
|
+
stripe_id: s[:id],
|
15
|
+
cancel_at: date(s[:cancel_at]),
|
16
|
+
canceled_at: date(s[:canceled_at]),
|
17
|
+
current_period_end: date(s[:current_period_end]),
|
18
|
+
current_period_start: date(s[:current_period_start]),
|
19
|
+
cancel_at_period_end: truthy?(s[:cancel_at_period_end]),
|
20
|
+
livemode: truthy?(s[:livemode]),
|
21
|
+
metadata: stripe_hash(s[:metadata]),
|
22
|
+
start: date(s[:start]),
|
23
|
+
start_date: date(s[:start_date]),
|
24
|
+
status: s[:status], # TODO: Status might not come from stripe?
|
25
|
+
trial_end: date(s[:trial_end]),
|
26
|
+
trial_start: date(s[:trial_start])
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
<% css_id = 'sp_form' if local_assigns[:css_id].nil? %>
|
2
|
+
<% user_id = nil if local_assigns[:user_id].nil? %>
|
3
|
+
<% plan_id = nil if local_assigns[:plan_id].nil? %>
|
4
|
+
|
5
|
+
<form action="<%= submit_path %>" method="post" class="payment-form" id="<%= css_id %>" style="display: none">
|
6
|
+
<%= hidden_field_tag :authenticity_token, form_authenticity_token -%>
|
7
|
+
<%= hidden_field_tag :user_id, user_id, class: 'form_user_id' -%>
|
8
|
+
<%= hidden_field_tag :plan_id, plan_id, class: 'form_plan_id' -%>
|
9
|
+
|
10
|
+
<div id="publishable-key" data-publishable="<%= SaasPayments.config.stripe_publishable_key %>"></div>
|
11
|
+
<div class="form-row">
|
12
|
+
<label for="card-element">
|
13
|
+
Credit or debit card
|
14
|
+
</label>
|
15
|
+
<div class="card-element" id="<%= css_id %>_card">
|
16
|
+
<!-- A Stripe Element will be inserted here. -->
|
17
|
+
</div>
|
18
|
+
|
19
|
+
<!-- Used to display form errors. -->
|
20
|
+
<div id="card-errors" role="alert">
|
21
|
+
<%= flash[:sp_error] %>
|
22
|
+
</div>
|
23
|
+
</div>
|
24
|
+
|
25
|
+
<button>Submit Payment</button>
|
26
|
+
</form>
|