saas_payments 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -7
  3. data/app/assets/javascripts/saas_payments/elements.js +2 -2
  4. data/app/controllers/concerns/saas_payments/payable.rb +189 -0
  5. data/app/controllers/concerns/saas_payments/webhook.rb +28 -0
  6. data/app/controllers/saas_payments/application_controller.rb +0 -1
  7. data/app/controllers/saas_payments/webhook_controller.rb +0 -2
  8. data/app/lib/saas_payments/products_service.rb +24 -9
  9. data/app/lib/saas_payments/webhook/customer_event.rb +5 -9
  10. data/app/lib/saas_payments/webhook/order_event.rb +31 -0
  11. data/app/lib/saas_payments/webhook/plan_event.rb +4 -4
  12. data/app/lib/saas_payments/webhook/product_event.rb +4 -4
  13. data/app/lib/saas_payments/webhook/session_event.rb +1 -1
  14. data/app/lib/saas_payments/webhook/sku_event.rb +25 -0
  15. data/app/lib/saas_payments/webhook/subscription_event.rb +4 -4
  16. data/app/lib/saas_payments/webhook_service.rb +14 -4
  17. data/app/models/concerns/saas_payments/stripe_model.rb +9 -11
  18. data/app/models/saas_payments/customer.rb +21 -13
  19. data/app/models/saas_payments/order.rb +28 -0
  20. data/app/models/saas_payments/plan.rb +2 -4
  21. data/app/models/saas_payments/product.rb +8 -9
  22. data/app/models/saas_payments/sku.rb +30 -0
  23. data/app/models/saas_payments/subscription.rb +2 -3
  24. data/app/views/saas_payments/_stripe_elements.html.erb +2 -0
  25. data/db/migrate/20191006144720_create_saas_payments_skus.rb +22 -0
  26. data/db/migrate/20191008134837_remove_user_id_from_customer.rb +5 -0
  27. data/db/migrate/20191010013513_create_saas_payments_orders.rb +24 -0
  28. data/lib/saas_payments/config.rb +0 -1
  29. data/lib/saas_payments/engine.rb +0 -1
  30. data/lib/saas_payments/version.rb +1 -1
  31. data/lib/tasks/products.rake +15 -1
  32. metadata +11 -4
  33. data/app/controllers/concerns/saas_payments/subscription_concerns.rb +0 -173
  34. data/app/controllers/concerns/saas_payments/webhook_concerns.rb +0 -22
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 399ad7181e74c3e135ffbf7c1990babb8d1aec389a453fe0b5b2414401bb6f9b
4
- data.tar.gz: da5bc44556fd4496ae64bece2da30f42cbdba80f77d79278714dad01681075f2
3
+ metadata.gz: ce17de72e16596e973ee323fe642f744afcbad16589fcd1ef0ac53c363dff461
4
+ data.tar.gz: 0e3fc7823f91df6b934fee91e3a8a82005075119a127400ecf70716977c66096
5
5
  SHA512:
6
- metadata.gz: da830a93f1a7c9a757fa2a21c2068296b6d40198479ff802d8740a09ff0ee297582adbc94f50b127ee315ad3e3e00d5e08efb6818bba62753ff24642af20e4f5
7
- data.tar.gz: 0765fa876818e55cd8b521199edf1adc58a77b96269bb54012a5ab1f74bebbab03555ac3f4571d98e02d8ad8b2479d82f79a9ad571157227944b79afd911facb
6
+ metadata.gz: '09f0fea75408fee1c60f4977b8ecd16ec9fd45167debe7705d8af9f406b0c0ac1310acd6a149b03ffc24f1d3499d40304e713d5e7ff58bf69ee4f035cebf2663'
7
+ data.tar.gz: cc114c1c7a728f9208d4109d66681c5fdc3996df3d92f09a4dd3ef7e0b52851bc62fb998b9c669b29094575b59c95bd16567388649801ba66957209096e6d31e
data/README.md CHANGED
@@ -1,15 +1,10 @@
1
1
  # SaasPayments
2
2
 
3
3
  SaasPayments is a thin wrapper around the Stripe API to manage the most common
4
- SAAS subscription actions. This gem includes
4
+ SAAS subscription actions.
5
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
6
 
11
-
12
- This Gem also assumes you already have:
7
+ This Gem assumes you already have:
13
8
 
14
9
  - A `User` model (If you don't, try [Devise](https://github.com/plataformatec/devise))
15
10
 
@@ -104,7 +104,7 @@ document.addEventListener('DOMContentLoaded', function() {
104
104
 
105
105
  var setHiddenValue = function(form, className, value) {
106
106
  var field = form.getElementsByClassName(className)[0]
107
- field.value = value
107
+ if(!!field) field.value = value
108
108
  }
109
109
 
110
110
  this.show = function(id) {
@@ -127,7 +127,7 @@ document.addEventListener('DOMContentLoaded', function() {
127
127
  return
128
128
  }
129
129
 
130
- var defaults = ['plan_id', 'user_id']
130
+ var defaults = ['sku_id', 'plan_id', 'user_id']
131
131
  for (var i = 0; i < defaults.length; i++) {
132
132
  var key = defaults[i]
133
133
  if(options[key]) setHiddenValue(this.form, 'form_'+key, options[key])
@@ -0,0 +1,189 @@
1
+ module SaasPayments
2
+ module Payable
3
+ extend ActiveSupport::Concern
4
+
5
+ Stripe.api_key = SaasPayments.config.stripe_secret_key
6
+
7
+ class FailedPayment < StandardError; end
8
+ class ActionRequired < StandardError; end
9
+
10
+ def purchase(customer, sku)
11
+ order = create_order customer, sku
12
+ pay_order customer, order
13
+ end
14
+
15
+ def sign_up_for_plan(customer, plan)
16
+ update_subscription customer, plan
17
+ render_success
18
+ rescue ActionRequired
19
+ render_action_required
20
+ rescue FailedPayment
21
+ render_card_failure
22
+ rescue Stripe::CardError
23
+ render_card_failure
24
+ end
25
+
26
+ def change_plan(customer, plan)
27
+ sub = customer.subscription remote: true
28
+ update_plan sub, customer, plan
29
+ render_success
30
+ rescue StandardError
31
+ render_error
32
+ end
33
+
34
+ def cancel_at_period_end(customer)
35
+ sub = customer.subscription remote: true
36
+
37
+ # Customer has an existing subscription, change plans
38
+ remote_sub = Stripe::Subscription.update(
39
+ sub[:id],
40
+ cancel_at_period_end: true
41
+ )
42
+
43
+ customer.subscription.update Subscription.from_stripe(remote_sub)
44
+ render_success
45
+ rescue StandardError
46
+ render_error
47
+ end
48
+
49
+ def resume_subscription(customer)
50
+ sub = customer.subscription
51
+ remote_sub = Stripe::Subscription.update(
52
+ sub.stripe_id,
53
+ cancel_at_period_end: false
54
+ )
55
+
56
+ customer.subscription.update Subscription.from_stripe(remote_sub)
57
+ render_success
58
+ rescue StandardError
59
+ render_error
60
+ end
61
+
62
+ def cancel_now(customer)
63
+ Stripe::Subscription.delete customer.subscription.stripe_id
64
+ end
65
+
66
+ def create_customer(email, card_token)
67
+ # Create a new customer in stripe
68
+ customer = Stripe::Customer.create(email: email, source: card_token)
69
+
70
+ # Create a new customer locally that maps to the stripe customer
71
+ Customer.create Customer.from_stripe(customer)
72
+ end
73
+
74
+ private
75
+
76
+ def create_order(customer, sku)
77
+ order = Stripe::Order.create({
78
+ currency: 'usd',
79
+ customer: customer.stripe_id,
80
+ items: [
81
+ { type: 'sku', parent: sku.stripe_id },
82
+ ],
83
+ })
84
+
85
+ Order.create Order.from_stripe(order)
86
+ end
87
+
88
+ def pay_order(customer, order)
89
+ # Pay the order
90
+ stripe_order = Stripe::Order.pay(
91
+ order[:stripe_id],
92
+ { customer: customer.stripe_id }
93
+ )
94
+ order.update Order.from_stripe(stripe_order)
95
+ order
96
+ end
97
+
98
+ def render_success
99
+ respond_to do |format|
100
+ format.html do
101
+ redirect_to SaasPayments.config.account_path
102
+ end
103
+ format.json do
104
+ render json: { message: "success" }
105
+ end
106
+ end
107
+ end
108
+
109
+ def render_error
110
+ format.html do
111
+ flash[:sp_error] = 'Failed to update plan'
112
+ redirect_back fallback_location: root_path
113
+ end
114
+ format.json { render json: { message: "update_failed" }, status: :bad_request }
115
+ end
116
+
117
+ def render_action_required
118
+ respond_to do |format|
119
+ format.html do
120
+ flash[:sp_notice] = 'Payment requires customer action'
121
+ redirect_to SaasPayments.successPath
122
+ end
123
+ format.json { render json: { message: "action_required" } }
124
+ end
125
+ end
126
+
127
+ def render_card_failure
128
+ respond_to do |format|
129
+ format.html do
130
+ flash[:sp_error] = 'Failed to process card'
131
+ redirect_back fallback_location: root_path
132
+ end
133
+ format.json { render json: { message: "card_error" }, status: :bad_request }
134
+ end
135
+ end
136
+
137
+ def update_subscription(customer, plan)
138
+ sub = customer.subscription remote: true
139
+ if sub.present?
140
+ update_plan sub, customer, plan
141
+ else
142
+ sub = create_subscription customer, plan
143
+ end
144
+
145
+ sub
146
+ end
147
+
148
+ def update_plan(sub, customer, plan)
149
+ # Customer has an existing subscription, change plans
150
+ remote_sub = Stripe::Subscription.update(
151
+ sub[:id],
152
+ cancel_at_period_end: false,
153
+ items: [
154
+ { id: sub[:items][:data][0][:id], plan: plan.stripe_id },
155
+ ]
156
+ )
157
+
158
+ customer.subscription.update Subscription.from_stripe(remote_sub)
159
+ end
160
+
161
+ def create_subscription(customer, plan)
162
+ # Customer does not have a subscription, sign them up
163
+ sub = Stripe::Subscription.create(
164
+ customer: customer.stripe_id,
165
+ items: [{ plan: plan.stripe_id }]
166
+ )
167
+ complete_subscription sub
168
+ end
169
+
170
+ def complete_subscription(sub)
171
+ if incomplete? sub
172
+ raise FailedPayment if payment_intent sub, :requires_payment_method
173
+ raise ActionRequired if payment_intent sub, :requires_action
174
+ end
175
+
176
+ # Successful payment, or trialling: provision the subscription
177
+ # Create a local subscription
178
+ Subscription.create Subscription.from_stripe(sub)
179
+ end
180
+
181
+ def incomplete?(sub)
182
+ sub[:status] == "incomplete"
183
+ end
184
+
185
+ def payment_intent(sub, intent)
186
+ sub[:latest_invoice][:payment_intent][:status] == intent.to_s
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,28 @@
1
+ module SaasPayments
2
+ module Webhook
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 = parse_event request.body.read
9
+ WebhookService.new(event).process
10
+ rescue ActiveRecord::RecordNotFound => e
11
+ raise SaasPayments::WebhookError, e.message
12
+ rescue StandardError => e
13
+ raise SaasPayments::WebhookError, e.message
14
+ end
15
+
16
+ private
17
+
18
+ def parse_event(payload)
19
+ if SaasPayments.config.webhook_secret.present?
20
+ header = request.headers['HTTP_STRIPE_SIGNATURE']
21
+ Stripe::Webhook.construct_event payload, header, SECRET
22
+ else
23
+ data = JSON.parse(request.body.read, symbolize_names: true)
24
+ Stripe::Event.construct_from(data)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -12,6 +12,5 @@ module SaasPayments
12
12
  def init_stripe
13
13
  Stripe.api_key = SaasPayments.config.stripe_secret_key || ENV["STRIPE_SECRET_KEY"]
14
14
  end
15
-
16
15
  end
17
16
  end
@@ -2,9 +2,7 @@ require_dependency "saas_payments/application_controller"
2
2
 
3
3
  module SaasPayments
4
4
  class WebhookController < ApplicationController
5
-
6
5
  def create
7
6
  end
8
-
9
7
  end
10
8
  end
@@ -1,24 +1,39 @@
1
1
  module SaasPayments
2
2
  class ProductsService
3
- def self.sync
3
+ def initialize
4
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 }
5
+ end
6
+
7
+ def sync
8
+ Stripe::Product.all.each { |p| create_product p }
9
+ Stripe::Plan.list.each { |p| create_plan p }
10
+ Stripe::SKU.list.each { |s| create_sku s }
11
+ end
12
+
13
+ def shippable(product, on)
14
+ p = Stripe::Product.retrieve(product)
15
+ p.shippable = on
16
+ p.save
17
+ rescue StandardError => e
18
+ puts "ERROR: #{e.message}"
19
+ false
7
20
  end
8
21
 
9
22
  private
10
23
 
11
- def self.create_product product
12
- Product.create!(Product.from_stripe(product))
24
+ def create_sku(sku)
25
+ Sku.create! Sku.from_stripe(sku)
26
+ end
27
+
28
+ def create_product(product)
29
+ Product.create! Product.from_stripe(product)
13
30
  rescue ActiveRecord::RecordInvalid
14
- puts "Product (#{product[:id]}) already exists. Updating..."
15
31
  Product.find_by_stripe_id(product[:id]).update!(Product.from_stripe(product))
16
32
  end
17
33
 
18
- def self.create_plan plan
19
- Plan.create!(Plan.from_stripe(plan))
34
+ def create_plan(plan)
35
+ Plan.create! Plan.from_stripe(plan)
20
36
  rescue ActiveRecord::RecordInvalid
21
- puts "Plan (#{plan[:id]}) already exists. Updating..."
22
37
  Plan.find_by_stripe_id(plan[:id]).update!(Plan.from_stripe(plan))
23
38
  end
24
39
  end
@@ -1,16 +1,16 @@
1
1
  module SaasPayments
2
2
  class Webhook::CustomerEvent
3
- def created data
4
- Customer.create(Customer.from_stripe(data).merge({ user_id: user_id(data) }))
3
+ def created(data)
4
+ Customer.create Customer.from_stripe(data)
5
5
  end
6
6
 
7
- def updated data
7
+ def updated(data)
8
8
  customer = Customer.find_by_stripe_id(data[:id])
9
9
  raise_not_found(data[:id]) unless customer
10
10
  customer.update(Customer.from_stripe(data))
11
11
  end
12
12
 
13
- def deleted data
13
+ def deleted(data)
14
14
  customer = Customer.find_by_stripe_id(data[:id])
15
15
  raise_not_found(data[:id]) unless customer
16
16
  customer.delete
@@ -18,11 +18,7 @@ module SaasPayments
18
18
 
19
19
  private
20
20
 
21
- def user_id data
22
- data[:metadata][:user_id] rescue nil
23
- end
24
-
25
- def raise_not_found id
21
+ def raise_not_found(id)
26
22
  raise ActiveRecord::RecordNotFound.new("Could not find customer with stripe id: #{id}")
27
23
  end
28
24
  end
@@ -0,0 +1,31 @@
1
+ module SaasPayments
2
+ class Webhook::OrderEvent
3
+ def created(data)
4
+ Order.create!(Order.from_stripe(data))
5
+ end
6
+
7
+ def updated(data)
8
+ order = Order.find_by_stripe_id(data[:id])
9
+ raise_not_found(data[:id]) unless order
10
+ order.update(Order.from_stripe(data))
11
+ end
12
+
13
+ def payment_failed(data)
14
+ order = Order.find_by_stripe_id(data[:id])
15
+ raise_not_found(data[:id]) unless order
16
+ order.update(Order.from_stripe(data))
17
+ end
18
+
19
+ def payment_succeeded(data)
20
+ order = Order.find_by_stripe_id(data[:id])
21
+ raise_not_found(data[:id]) unless order
22
+ order.update(Order.from_stripe(data))
23
+ end
24
+
25
+ private
26
+
27
+ def raise_not_found(id)
28
+ raise ActiveRecord::RecordNotFound.new("Could not find order with stripe_id: #{id}")
29
+ end
30
+ end
31
+ end
@@ -1,16 +1,16 @@
1
1
  module SaasPayments
2
2
  class Webhook::PlanEvent
3
- def created data
3
+ def created(data)
4
4
  Plan.create!(Plan.from_stripe(data))
5
5
  end
6
6
 
7
- def updated data
7
+ def updated(data)
8
8
  plan = Plan.find_by_stripe_id(data[:id])
9
9
  raise_not_found(data[:id]) unless plan
10
10
  plan.update(Plan.from_stripe(data))
11
11
  end
12
12
 
13
- def deleted data
13
+ def deleted(data)
14
14
  plan = Plan.find_by_stripe_id(data[:id])
15
15
  raise_not_found(data[:id]) unless plan
16
16
  plan.delete
@@ -18,7 +18,7 @@ module SaasPayments
18
18
 
19
19
  private
20
20
 
21
- def raise_not_found id
21
+ def raise_not_found(id)
22
22
  raise ActiveRecord::RecordNotFound.new("Could not find plan with stripe id: #{id}")
23
23
  end
24
24
  end
@@ -1,16 +1,16 @@
1
1
  module SaasPayments
2
2
  class Webhook::ProductEvent
3
- def created data
3
+ def created(data)
4
4
  Product.create!(Product.from_stripe(data))
5
5
  end
6
6
 
7
- def updated data
7
+ def updated(data)
8
8
  product = Product.find_by_stripe_id(data[:id])
9
9
  raise_not_found(data[:id]) unless product
10
10
  product.update(Product.from_stripe(data))
11
11
  end
12
12
 
13
- def deleted data
13
+ def deleted(data)
14
14
  product = Product.find_by_stripe_id(data[:id])
15
15
  raise_not_found(data[:id]) unless product
16
16
  product.delete
@@ -18,7 +18,7 @@ module SaasPayments
18
18
 
19
19
  private
20
20
 
21
- def raise_not_found id
21
+ def raise_not_found(id)
22
22
  raise ActiveRecord::RecordNotFound.new("Could not find product with stripe_id: #{id}")
23
23
  end
24
24
  end
@@ -1,6 +1,6 @@
1
1
  module SaasPayments
2
2
  class Webhook::SessionEvent
3
- def completed data
3
+ def completed(data)
4
4
  end
5
5
  end
6
6
  end
@@ -0,0 +1,25 @@
1
+ module SaasPayments
2
+ class Webhook::SkuEvent
3
+ def created(data)
4
+ Sku.create!(Sku.from_stripe(data))
5
+ end
6
+
7
+ def updated(data)
8
+ sku = Sku.find_by_stripe_id(data[:id])
9
+ raise_not_found(data[:id]) unless sku
10
+ sku.update(Sku.from_stripe(data))
11
+ end
12
+
13
+ def deleted(data)
14
+ sku = Sku.find_by_stripe_id(data[:id])
15
+ raise_not_found(data[:id]) unless sku
16
+ sku.delete
17
+ end
18
+
19
+ private
20
+
21
+ def raise_not_found(id)
22
+ raise ActiveRecord::RecordNotFound.new("Could not find sku with stripe id: #{id}")
23
+ end
24
+ end
25
+ end
@@ -1,16 +1,16 @@
1
1
  module SaasPayments
2
2
  class Webhook::SubscriptionEvent
3
- def created data
3
+ def created(data)
4
4
  Subscription.create!(Subscription.from_stripe(data))
5
5
  end
6
6
 
7
- def updated data
7
+ def updated(data)
8
8
  sub = Subscription.find_by_stripe_id(data[:id])
9
9
  raise_not_found(data[:id]) unless sub
10
10
  sub.update(Subscription.from_stripe(data))
11
11
  end
12
12
 
13
- def deleted data
13
+ def deleted(data)
14
14
  sub = Subscription.find_by_stripe_id(data[:id])
15
15
  raise_not_found(data[:id]) unless sub
16
16
  sub.delete
@@ -18,7 +18,7 @@ module SaasPayments
18
18
 
19
19
  private
20
20
 
21
- def raise_not_found id
21
+ def raise_not_found(id)
22
22
  raise ActiveRecord::RecordNotFound.new("Could not find subscription with stripe id: #{id}")
23
23
  end
24
24
  end
@@ -11,6 +11,11 @@ module SaasPayments
11
11
  'plan.deleted' => [Webhook::PlanEvent, 'deleted'],
12
12
  'plan.updated' => [Webhook::PlanEvent, 'updated'],
13
13
 
14
+ # Register Plan events
15
+ 'sku.created' => [Webhook::SkuEvent, 'created'],
16
+ 'sku.deleted' => [Webhook::SkuEvent, 'deleted'],
17
+ 'sku.updated' => [Webhook::SkuEvent, 'updated'],
18
+
14
19
  # Register Customer events
15
20
  'customer.created' => [Webhook::CustomerEvent, 'created'],
16
21
  'customer.deleted' => [Webhook::CustomerEvent, 'deleted'],
@@ -22,10 +27,16 @@ module SaasPayments
22
27
  'customer.subscription.updated' => [Webhook::SubscriptionEvent, 'updated'],
23
28
 
24
29
  # Register Session events
25
- 'checkout.session.completed' => [Webhook::SessionEvent, 'completed']
26
- }
30
+ 'checkout.session.completed' => [Webhook::SessionEvent, 'completed'],
27
31
 
28
- def initialize event
32
+ # Order events
33
+ 'order.created' => [Webhook::OrderEvent, 'created'],
34
+ 'order.payment_failed' => [Webhook::OrderEvent, 'payment_failed'],
35
+ 'order.payment_succeeded' => [Webhook::OrderEvent, 'payment_succeeded'],
36
+ 'order.updated' => [Webhook::OrderEvent, 'updated'],
37
+ }.freeze
38
+
39
+ def initialize(event)
29
40
  @event = event
30
41
  end
31
42
 
@@ -34,6 +45,5 @@ module SaasPayments
34
45
  raise WebhookError.new("Unrecognized event") unless event && action
35
46
  event.new.send(action, @event[:data][:object])
36
47
  end
37
-
38
48
  end
39
49
  end
@@ -3,30 +3,28 @@ module SaasPayments
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  module ClassMethods
6
- def truthy? val
6
+ def truthy?(val)
7
7
  val.present? ? val : false
8
8
  end
9
9
 
10
- def stripe_hash data
10
+ def stripe_hash(data)
11
11
  (data || {}).to_hash
12
12
  end
13
13
 
14
- def date ts
14
+ def stripe_array(data)
15
+ (data || []).map(&:to_hash)
16
+ end
17
+
18
+ def date(ts)
15
19
  ts.present? ? Time.at(ts).to_datetime : nil
16
20
  rescue
17
21
  nil
18
22
  end
19
23
 
20
- def parse_id val
24
+ def parse_id(val)
21
25
  return nil unless val
22
-
23
- if val.is_a?(String)
24
- val
25
- else
26
- val[:id]
27
- end
26
+ val.is_a?(String) ? val : val[:id]
28
27
  end
29
28
  end
30
-
31
29
  end
32
30
  end
@@ -4,25 +4,23 @@ module SaasPayments
4
4
 
5
5
  validates_uniqueness_of :stripe_id
6
6
 
7
- belongs_to :user, optional: true
8
-
9
7
  serialize :discount
10
8
  serialize :metadata
11
9
 
12
- def self.from_stripe c
10
+ def self.from_stripe(c)
13
11
  {
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]
12
+ stripe_id: c[:id],
13
+ delinquent: truthy?(c[:delinquent]),
14
+ description: c[:description],
15
+ discount: stripe_hash(c[:discount]),
16
+ email: c[:email],
17
+ livemode: truthy?(c[:livemode]),
18
+ metadata: stripe_hash(c[:metadata]),
19
+ name: c[:name],
22
20
  }
23
21
  end
24
22
 
25
- def subscription options={}
23
+ def subscription(options = {})
26
24
  sub = Subscription.where(customer_id: stripe_id).first
27
25
  if sub.present? && options[:remote]
28
26
  sub = Stripe::Subscription.retrieve(sub.stripe_id)
@@ -34,9 +32,19 @@ module SaasPayments
34
32
  end
35
33
 
36
34
  def plan
37
- return nil if !subscription.present?
35
+ return nil if subscription.blank?
38
36
  Plan.find_by_stripe_id(subscription.plan_id)
39
37
  end
40
38
 
39
+ def products
40
+ items = []
41
+ Order.where(customer_id: stripe_id).map do |order|
42
+ order.items.each do |item|
43
+ sku = Sku.find_by_stripe_id(item[:parent].to_s)
44
+ items << sku if sku.present?
45
+ end
46
+ end
47
+ items
48
+ end
41
49
  end
42
50
  end
@@ -0,0 +1,28 @@
1
+ module SaasPayments
2
+ class Order < ApplicationRecord
3
+ include StripeModel
4
+
5
+ serialize :metadata
6
+ serialize :status_transitions
7
+ serialize :items
8
+
9
+ def self.from_stripe(o)
10
+ {
11
+ stripe_id: parse_id(o[:id]),
12
+ customer_id: parse_id(o[:customer]),
13
+ amount: o[:amount],
14
+ amount_returned: o[:amount_returned],
15
+ application: o[:application],
16
+ application_fee: o[:application_fee],
17
+ charge: o[:charge],
18
+ currency: o[:currency],
19
+ email: o[:email],
20
+ livemode: o[:livemode],
21
+ status: o[:status],
22
+ metadata: stripe_hash(o[:metadata]),
23
+ status_transitions: stripe_hash(o[:status_transitions]),
24
+ items: stripe_array(o[:items]),
25
+ }
26
+ end
27
+ end
28
+ end
@@ -6,10 +6,9 @@ module SaasPayments
6
6
 
7
7
  serialize :metadata
8
8
 
9
- def self.from_stripe p
9
+ def self.from_stripe(p)
10
10
  {
11
11
  product_id: parse_id(p[:product]),
12
-
13
12
  stripe_id: p[:id],
14
13
  active: p[:active],
15
14
  amount: p[:amount].to_i,
@@ -20,9 +19,8 @@ module SaasPayments
20
19
  livemode: truthy?(p[:livemode]),
21
20
  metadata: stripe_hash(p[:metadata]),
22
21
  nickname: p[:nickname],
23
- trial_period_days: p[:trial_period_days].to_i
22
+ trial_period_days: p[:trial_period_days].to_i,
24
23
  }
25
-
26
24
  end
27
25
  end
28
26
  end
@@ -8,17 +8,16 @@ module SaasPayments
8
8
 
9
9
  serialize :metadata
10
10
 
11
- def self.from_stripe p
11
+ def self.from_stripe(p)
12
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],
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
20
  }
21
21
  end
22
-
23
22
  end
24
23
  end
@@ -0,0 +1,30 @@
1
+ module SaasPayments
2
+ class Sku < ApplicationRecord
3
+ include StripeModel
4
+
5
+ serialize :metadata
6
+ serialize :attrs
7
+ serialize :inventory
8
+ serialize :package_dimensions
9
+
10
+ def self.from_stripe(p)
11
+ {
12
+ product_id: parse_id(p[:product]),
13
+ stripe_id: p[:id],
14
+ active: p[:active],
15
+ currency: p[:currency],
16
+ image: p[:image],
17
+ livemode: truthy?(p[:livemode]),
18
+ price: p[:price].to_i,
19
+ package_dimensions: stripe_hash(p[:package_dimensions]),
20
+ metadata: stripe_hash(p[:metadata]),
21
+ inventory: stripe_hash(p[:inventory]),
22
+ attrs: stripe_hash(p[:attributes]),
23
+ }
24
+ end
25
+
26
+ def name
27
+ attrs[:name]
28
+ end
29
+ end
30
+ end
@@ -6,7 +6,7 @@ module SaasPayments
6
6
 
7
7
  serialize :metadata
8
8
 
9
- def self.from_stripe s
9
+ def self.from_stripe(s)
10
10
  {
11
11
  plan_id: parse_id(s[:plan]),
12
12
  customer_id: parse_id(s[:customer]),
@@ -23,9 +23,8 @@ module SaasPayments
23
23
  start_date: date(s[:start_date]),
24
24
  status: s[:status], # TODO: Status might not come from stripe?
25
25
  trial_end: date(s[:trial_end]),
26
- trial_start: date(s[:trial_start])
26
+ trial_start: date(s[:trial_start]),
27
27
  }
28
28
  end
29
-
30
29
  end
31
30
  end
@@ -1,11 +1,13 @@
1
1
  <% css_id = 'sp_form' if local_assigns[:css_id].nil? %>
2
2
  <% user_id = nil if local_assigns[:user_id].nil? %>
3
3
  <% plan_id = nil if local_assigns[:plan_id].nil? %>
4
+ <% sku_id = nil if local_assigns[:sku_id].nil? %>
4
5
 
5
6
  <form action="<%= submit_path %>" method="post" class="payment-form" id="<%= css_id %>" style="display: none">
6
7
  <%= hidden_field_tag :authenticity_token, form_authenticity_token -%>
7
8
  <%= hidden_field_tag :user_id, user_id, class: 'form_user_id' -%>
8
9
  <%= hidden_field_tag :plan_id, plan_id, class: 'form_plan_id' -%>
10
+ <%= hidden_field_tag :sku_id, sku_id, class: 'form_sku_id' -%>
9
11
 
10
12
  <div id="publishable-key" data-publishable="<%= SaasPayments.config.stripe_publishable_key %>"></div>
11
13
  <div class="form-row">
@@ -0,0 +1,22 @@
1
+ class CreateSaasPaymentsSkus < ActiveRecord::Migration[5.2]
2
+ def change
3
+ create_table :saas_payments_skus do |t|
4
+ t.string :stripe_id
5
+ t.string :product_id
6
+
7
+ t.boolean :active
8
+ t.string :currency
9
+ t.string :image
10
+ t.boolean :livemode
11
+ t.integer :price
12
+
13
+ # Serialized objects
14
+ t.text :package_dimensions
15
+ t.text :metadata
16
+ t.text :inventory
17
+ t.text :attrs
18
+
19
+ t.timestamps
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,5 @@
1
+ class RemoveUserIdFromCustomer < ActiveRecord::Migration[5.2]
2
+ def change
3
+ remove_column :saas_payments_customers, :user_id
4
+ end
5
+ end
@@ -0,0 +1,24 @@
1
+ class CreateSaasPaymentsOrders < ActiveRecord::Migration[5.2]
2
+ def change
3
+ create_table :saas_payments_orders do |t|
4
+ t.string :stripe_id
5
+ t.string :customer_id
6
+ t.integer :amount
7
+ t.integer :amount_returned
8
+ t.string :application
9
+ t.integer :application_fee
10
+ t.string :charge
11
+ t.string :currency
12
+ t.string :email
13
+ t.boolean :livemode
14
+ t.string :status
15
+
16
+ # Serialize
17
+ t.text :metadata
18
+ t.text :status_transitions
19
+ t.text :items
20
+
21
+ t.timestamps
22
+ end
23
+ end
24
+ end
@@ -1,4 +1,3 @@
1
-
2
1
  module SaasPayments
3
2
  class Configuration
4
3
  attr_accessor :stripe_secret_key
@@ -14,5 +14,4 @@ module SaasPayments
14
14
  yield(self)
15
15
  end
16
16
  end
17
-
18
17
  end
@@ -1,3 +1,3 @@
1
1
  module SaasPayments
2
- VERSION = '0.0.2'
2
+ VERSION = '0.1.0'
3
3
  end
@@ -1,6 +1,20 @@
1
1
  namespace :products do
2
2
  desc "Create products in the database that come from stripe"
3
3
  task sync: :environment do
4
- SaasPayments::ProductsService.sync
4
+ SaasPayments::ProductsService.new.sync
5
+ end
6
+
7
+ desc "Digitize a product by toggling the shippable flag"
8
+ task :digitize, [:product, :shippable] => :environment do |t, args|
9
+ product = args.product
10
+ shippable = args.shippable.to_s == 'true'
11
+
12
+ if !['true', 'false'].include? args.shippable.to_s
13
+ puts "Shippable must be a boolean"
14
+ next
15
+ end
16
+
17
+ success = SaasPayments::ProductsService.new.shippable product, shippable
18
+ puts "Could not digitize product: #{args.product}" unless success
5
19
  end
6
20
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: saas_payments
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cody
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-09-03 00:00:00.000000000 Z
11
+ date: 2019-10-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -123,25 +123,29 @@ files:
123
123
  - app/assets/javascripts/saas_payments/elements.js
124
124
  - app/assets/stylesheets/saas_payments/application.css
125
125
  - app/assets/stylesheets/saas_payments/elements.css
126
- - app/controllers/concerns/saas_payments/subscription_concerns.rb
127
- - app/controllers/concerns/saas_payments/webhook_concerns.rb
126
+ - app/controllers/concerns/saas_payments/payable.rb
127
+ - app/controllers/concerns/saas_payments/webhook.rb
128
128
  - app/controllers/saas_payments/application_controller.rb
129
129
  - app/controllers/saas_payments/webhook_controller.rb
130
130
  - app/helpers/saas_payments/application_helper.rb
131
131
  - app/jobs/saas_payments/application_job.rb
132
132
  - app/lib/saas_payments/products_service.rb
133
133
  - app/lib/saas_payments/webhook/customer_event.rb
134
+ - app/lib/saas_payments/webhook/order_event.rb
134
135
  - app/lib/saas_payments/webhook/plan_event.rb
135
136
  - app/lib/saas_payments/webhook/product_event.rb
136
137
  - app/lib/saas_payments/webhook/session_event.rb
138
+ - app/lib/saas_payments/webhook/sku_event.rb
137
139
  - app/lib/saas_payments/webhook/subscription_event.rb
138
140
  - app/lib/saas_payments/webhook_service.rb
139
141
  - app/mailers/saas_payments/application_mailer.rb
140
142
  - app/models/concerns/saas_payments/stripe_model.rb
141
143
  - app/models/saas_payments/application_record.rb
142
144
  - app/models/saas_payments/customer.rb
145
+ - app/models/saas_payments/order.rb
143
146
  - app/models/saas_payments/plan.rb
144
147
  - app/models/saas_payments/product.rb
148
+ - app/models/saas_payments/sku.rb
145
149
  - app/models/saas_payments/subscription.rb
146
150
  - app/views/saas_payments/_scripts.html.erb
147
151
  - app/views/saas_payments/_stripe_elements.html.erb
@@ -151,6 +155,9 @@ files:
151
155
  - db/migrate/20190820040835_create_saas_payments_products.rb
152
156
  - db/migrate/20190820040959_create_saas_payments_plans.rb
153
157
  - db/migrate/20190823023237_create_saas_payments_customers.rb
158
+ - db/migrate/20191006144720_create_saas_payments_skus.rb
159
+ - db/migrate/20191008134837_remove_user_id_from_customer.rb
160
+ - db/migrate/20191010013513_create_saas_payments_orders.rb
154
161
  - lib/saas_payments.rb
155
162
  - lib/saas_payments/config.rb
156
163
  - lib/saas_payments/engine.rb
@@ -1,173 +0,0 @@
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
@@ -1,22 +0,0 @@
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