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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +146 -0
  4. data/Rakefile +32 -0
  5. data/app/assets/config/saas_payments_manifest.js +2 -0
  6. data/app/assets/javascripts/saas_payments/application.js +15 -0
  7. data/app/assets/javascripts/saas_payments/elements.js +146 -0
  8. data/app/assets/stylesheets/saas_payments/application.css +15 -0
  9. data/app/assets/stylesheets/saas_payments/elements.css +31 -0
  10. data/app/controllers/concerns/saas_payments/subscription_concerns.rb +173 -0
  11. data/app/controllers/concerns/saas_payments/webhook_concerns.rb +22 -0
  12. data/app/controllers/saas_payments/application_controller.rb +17 -0
  13. data/app/controllers/saas_payments/webhook_controller.rb +10 -0
  14. data/app/helpers/saas_payments/application_helper.rb +4 -0
  15. data/app/jobs/saas_payments/application_job.rb +4 -0
  16. data/app/lib/saas_payments/products_service.rb +25 -0
  17. data/app/lib/saas_payments/webhook/customer_event.rb +29 -0
  18. data/app/lib/saas_payments/webhook/plan_event.rb +25 -0
  19. data/app/lib/saas_payments/webhook/product_event.rb +25 -0
  20. data/app/lib/saas_payments/webhook/session_event.rb +6 -0
  21. data/app/lib/saas_payments/webhook/subscription_event.rb +25 -0
  22. data/app/lib/saas_payments/webhook_service.rb +39 -0
  23. data/app/mailers/saas_payments/application_mailer.rb +6 -0
  24. data/app/models/concerns/saas_payments/stripe_model.rb +32 -0
  25. data/app/models/saas_payments/application_record.rb +5 -0
  26. data/app/models/saas_payments/customer.rb +42 -0
  27. data/app/models/saas_payments/plan.rb +28 -0
  28. data/app/models/saas_payments/product.rb +24 -0
  29. data/app/models/saas_payments/subscription.rb +31 -0
  30. data/app/views/saas_payments/_scripts.html.erb +5 -0
  31. data/app/views/saas_payments/_stripe_elements.html.erb +26 -0
  32. data/config/initializers/stripe.rb +0 -0
  33. data/config/routes.rb +4 -0
  34. data/db/migrate/20190818184232_create_saas_payments_subscriptions.rb +28 -0
  35. data/db/migrate/20190820040835_create_saas_payments_products.rb +19 -0
  36. data/db/migrate/20190820040959_create_saas_payments_plans.rb +25 -0
  37. data/db/migrate/20190823023237_create_saas_payments_customers.rb +19 -0
  38. data/lib/saas_payments.rb +15 -0
  39. data/lib/saas_payments/config.rb +9 -0
  40. data/lib/saas_payments/engine.rb +18 -0
  41. data/lib/saas_payments/errors.rb +3 -0
  42. data/lib/saas_payments/version.rb +3 -0
  43. data/lib/tasks/products.rake +6 -0
  44. data/lib/tasks/saas_payments_tasks.rake +4 -0
  45. 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,10 @@
1
+ require_dependency "saas_payments/application_controller"
2
+
3
+ module SaasPayments
4
+ class WebhookController < ApplicationController
5
+
6
+ def create
7
+ end
8
+
9
+ end
10
+ end
@@ -0,0 +1,4 @@
1
+ module SaasPayments
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module SaasPayments
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ 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,6 @@
1
+ module SaasPayments
2
+ class Webhook::SessionEvent
3
+ def completed data
4
+ end
5
+ end
6
+ 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,6 @@
1
+ module SaasPayments
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: 'from@example.com'
4
+ layout 'mailer'
5
+ end
6
+ 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,5 @@
1
+ module SaasPayments
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ 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,5 @@
1
+ <script id="stripeSrc" src="https://js.stripe.com/v3/"></script>
2
+
3
+ <%= stylesheet_link_tag "saas_payments/elements" %>
4
+ <%= javascript_include_tag "saas_payments/elements" %>
5
+
@@ -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>