saas_payments 0.0.2 → 0.1.0

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 (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