pay 2.6.8 → 2.7.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of pay might be problematic. Click here for more details.

Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +73 -50
  3. data/app/models/pay.rb +5 -0
  4. data/app/models/pay/charge.rb +2 -0
  5. data/app/models/pay/subscription.rb +9 -2
  6. data/db/migrate/20200603134434_add_data_to_pay_models.rb +2 -18
  7. data/db/migrate/20210309004259_add_data_to_pay_billable.rb +10 -0
  8. data/db/migrate/20210406215234_add_currency_to_pay_charges.rb +5 -0
  9. data/db/migrate/20210406215506_add_application_fee_to_pay_models.rb +7 -0
  10. data/lib/generators/active_record/billable_generator.rb +44 -0
  11. data/lib/generators/active_record/merchant_generator.rb +44 -0
  12. data/lib/generators/active_record/templates/billable_migration.rb +16 -0
  13. data/lib/generators/active_record/templates/merchant_migration.rb +12 -0
  14. data/lib/generators/pay/{pay_generator.rb → billable_generator.rb} +2 -3
  15. data/lib/generators/pay/merchant_generator.rb +17 -0
  16. data/lib/generators/pay/orm_helpers.rb +10 -6
  17. data/lib/pay.rb +22 -0
  18. data/lib/pay/adapter.rb +22 -0
  19. data/lib/pay/billable.rb +4 -0
  20. data/lib/pay/braintree/billable.rb +11 -2
  21. data/lib/pay/braintree/subscription.rb +4 -0
  22. data/lib/pay/env.rb +8 -0
  23. data/lib/pay/fake_processor/subscription.rb +4 -0
  24. data/lib/pay/merchant.rb +37 -0
  25. data/lib/pay/paddle/subscription.rb +7 -0
  26. data/lib/pay/paddle/webhooks/subscription_payment_succeeded.rb +3 -1
  27. data/lib/pay/stripe.rb +11 -0
  28. data/lib/pay/stripe/billable.rb +38 -47
  29. data/lib/pay/stripe/charge.rb +38 -4
  30. data/lib/pay/stripe/merchant.rb +66 -0
  31. data/lib/pay/stripe/subscription.rb +64 -20
  32. data/lib/pay/stripe/webhooks/account_updated.rb +17 -0
  33. data/lib/pay/stripe/webhooks/charge_refunded.rb +2 -7
  34. data/lib/pay/stripe/webhooks/charge_succeeded.rb +2 -8
  35. data/lib/pay/stripe/webhooks/checkout_session_async_payment_succeeded.rb +13 -0
  36. data/lib/pay/stripe/webhooks/checkout_session_completed.rb +13 -0
  37. data/lib/pay/stripe/webhooks/payment_intent_succeeded.rb +2 -8
  38. data/lib/pay/stripe/webhooks/subscription_created.rb +1 -35
  39. data/lib/pay/stripe/webhooks/subscription_deleted.rb +1 -9
  40. data/lib/pay/stripe/webhooks/subscription_renewing.rb +4 -6
  41. data/lib/pay/stripe/webhooks/subscription_updated.rb +1 -28
  42. data/lib/pay/version.rb +1 -1
  43. metadata +19 -6
  44. data/lib/generators/active_record/pay_generator.rb +0 -58
  45. data/lib/generators/active_record/templates/migration.rb +0 -9
@@ -3,20 +3,54 @@ module Pay
3
3
  class Charge
4
4
  attr_reader :pay_charge
5
5
 
6
- delegate :processor_id, :owner, to: :pay_charge
6
+ delegate :processor_id, :owner, :stripe_account, to: :pay_charge
7
+
8
+ def self.sync(charge_id, object: nil)
9
+ object ||= ::Stripe::Charge.retrieve(id: charge_id)
10
+ owner = Pay.find_billable(processor: :stripe, processor_id: object.customer)
11
+ return unless owner
12
+
13
+ attrs = {
14
+ amount: object.amount,
15
+ amount_refunded: object.amount_refunded,
16
+ application_fee_amount: object.application_fee_amount,
17
+ card_exp_month: object.payment_method_details.card.exp_month,
18
+ card_exp_year: object.payment_method_details.card.exp_year,
19
+ card_last4: object.payment_method_details.card.last4,
20
+ card_type: object.payment_method_details.card.brand,
21
+ created_at: Time.at(object.created),
22
+ currency: object.currency,
23
+ stripe_account: owner.stripe_account
24
+ }
25
+
26
+ # Associate charge with subscription if we can
27
+ if object.invoice
28
+ invoice = (object.invoice.is_a?(::Stripe::Invoice) ? object.invoice : ::Stripe::Invoice.retrieve(object.invoice))
29
+ attrs[:subscription] = Pay::Subscription.find_by(processor: :stripe, processor_id: invoice.subscription)
30
+ end
31
+
32
+ pay_charge = owner.charges.find_or_initialize_by(processor: :stripe, processor_id: object.id)
33
+ pay_charge.update(attrs)
34
+ pay_charge
35
+ end
7
36
 
8
37
  def initialize(pay_charge)
9
38
  @pay_charge = pay_charge
10
39
  end
11
40
 
12
41
  def charge
13
- ::Stripe::Charge.retrieve(processor_id)
42
+ ::Stripe::Charge.retrieve({id: processor_id, expand: ["customer", "invoice.subscription"]}, {stripe_account: stripe_account})
14
43
  rescue ::Stripe::StripeError => e
15
44
  raise Pay::Stripe::Error, e
16
45
  end
17
46
 
18
- def refund!(amount_to_refund)
19
- ::Stripe::Refund.create(charge: processor_id, amount: amount_to_refund)
47
+ # https://stripe.com/docs/api/refunds/create
48
+ #
49
+ # refund!
50
+ # refund!(5_00)
51
+ # refund!(5_00, refund_application_fee: true)
52
+ def refund!(amount_to_refund, **options)
53
+ ::Stripe::Refund.create(options.merge(charge: processor_id, amount: amount_to_refund), {stripe_account: stripe_account})
20
54
  pay_charge.update(amount_refunded: amount_to_refund)
21
55
  rescue ::Stripe::StripeError => e
22
56
  raise Pay::Stripe::Error, e
@@ -0,0 +1,66 @@
1
+ module Pay
2
+ module Stripe
3
+ class Merchant
4
+ attr_reader :merchant
5
+
6
+ delegate :stripe_connect_account_id,
7
+ to: :merchant
8
+
9
+ def initialize(merchant)
10
+ @merchant = merchant
11
+ end
12
+
13
+ def create_account(**options)
14
+ defaults = {
15
+ type: "express",
16
+ capabilities: {
17
+ card_payments: {requested: true},
18
+ transfers: {requested: true}
19
+ }
20
+ }
21
+
22
+ stripe_account = ::Stripe::Account.create(defaults.merge(options))
23
+ merchant.update(stripe_connect_account_id: stripe_account.id)
24
+ stripe_account
25
+ rescue ::Stripe::StripeError => e
26
+ raise Pay::Stripe::Error, e
27
+ end
28
+
29
+ def account
30
+ ::Stripe::Account.retrieve(stripe_connect_account_id)
31
+ rescue ::Stripe::StripeError => e
32
+ raise Pay::Stripe::Error, e
33
+ end
34
+
35
+ def account_link(refresh_url:, return_url:, type: "account_onboarding", **options)
36
+ ::Stripe::AccountLink.create({
37
+ account: stripe_connect_account_id,
38
+ refresh_url: refresh_url,
39
+ return_url: return_url,
40
+ type: type
41
+ })
42
+ rescue ::Stripe::StripeError => e
43
+ raise Pay::Stripe::Error, e
44
+ end
45
+
46
+ # A single-use login link for Express accounts to access their Stripe dashboard
47
+ def login_link(**options)
48
+ ::Stripe::Account.create_login_link(stripe_connect_account_id)
49
+ rescue ::Stripe::StripeError => e
50
+ raise Pay::Stripe::Error, e
51
+ end
52
+
53
+ # Transfer money from the platform to this connected account
54
+ # https://stripe.com/docs/connect/charges-transfers#transfer-availability
55
+ def transfer(amount:, currency: "usd", **options)
56
+ ::Stripe::Transfer.create({
57
+ amount: amount,
58
+ currency: currency,
59
+ destination: stripe_connect_account_id
60
+ }.merge(options))
61
+ rescue ::Stripe::StripeError => e
62
+ raise Pay::Stripe::Error, e
63
+ end
64
+ end
65
+ end
66
+ end
@@ -16,27 +16,63 @@ module Pay
16
16
  :prorate?,
17
17
  :quantity,
18
18
  :quantity?,
19
+ :stripe_account,
19
20
  :trial_ends_at,
20
21
  to: :pay_subscription
21
22
 
23
+ def self.sync(subscription_id, object: nil, name: Pay.default_product_name)
24
+ # Skip loading the latest subscription details from the API if we already have it
25
+ object ||= ::Stripe::Subscription.retrieve(id: subscription_id, expand: ["pending_setup_intent", "latest_invoice.payment_intent"])
26
+
27
+ owner = Pay.find_billable(processor: :stripe, processor_id: object.customer)
28
+ return unless owner
29
+
30
+ attributes = {
31
+ application_fee_percent: object.application_fee_percent,
32
+ processor_plan: object.plan.id,
33
+ quantity: object.quantity,
34
+ name: name,
35
+ status: object.status,
36
+ stripe_account: owner.stripe_account,
37
+ trial_ends_at: (object.trial_end ? Time.at(object.trial_end) : nil)
38
+ }
39
+
40
+ # Subscriptions cancelling in the future
41
+ attributes[:ends_at] = Time.at(object.current_period_end) if object.cancel_at_period_end
42
+
43
+ # Fully cancelled subscription
44
+ attributes[:ends_at] = Time.at(object.ended_at) if object.ended_at
45
+
46
+ # Update or create the subscription
47
+ pay_subscription = owner.subscriptions.find_or_initialize_by(processor: :stripe, processor_id: object.id)
48
+ pay_subscription.update(attributes)
49
+ pay_subscription
50
+ end
51
+
22
52
  def initialize(pay_subscription)
23
53
  @pay_subscription = pay_subscription
24
54
  end
25
55
 
26
- def cancel
27
- subscription = processor_subscription
28
- subscription.cancel_at_period_end = true
29
- subscription.save
56
+ def subscription(**options)
57
+ ::Stripe::Subscription.retrieve(options.merge(id: processor_id))
58
+ end
30
59
 
31
- new_ends_at = on_trial? ? trial_ends_at : Time.at(subscription.current_period_end)
32
- pay_subscription.update(ends_at: new_ends_at)
60
+ def cancel
61
+ stripe_sub = ::Stripe::Subscription.update(processor_id, {cancel_at_period_end: true}, {stripe_account: stripe_account})
62
+ pay_subscription.update(ends_at: (on_trial? ? trial_ends_at : Time.at(stripe_sub.current_period_end)))
33
63
  rescue ::Stripe::StripeError => e
34
64
  raise Pay::Stripe::Error, e
35
65
  end
36
66
 
37
67
  def cancel_now!
38
- processor_subscription.delete
39
- pay_subscription.update(ends_at: Time.zone.now, status: :canceled)
68
+ ::Stripe::Subscription.delete(processor_id, {stripe_account: stripe_account})
69
+ pay_subscription.update(ends_at: Time.current, status: :canceled)
70
+ rescue ::Stripe::StripeError => e
71
+ raise Pay::Stripe::Error, e
72
+ end
73
+
74
+ def change_quantity(quantity)
75
+ ::Stripe::Subscription.update(processor_id, quantity: quantity)
40
76
  rescue ::Stripe::StripeError => e
41
77
  raise Pay::Stripe::Error, e
42
78
  end
@@ -58,23 +94,31 @@ module Pay
58
94
  raise StandardError, "You can only resume subscriptions within their grace period."
59
95
  end
60
96
 
61
- subscription = processor_subscription
62
- subscription.plan = processor_plan
63
- subscription.trial_end = on_trial? ? trial_ends_at.to_i : "now"
64
- subscription.cancel_at_period_end = false
65
- subscription.save
97
+ ::Stripe::Subscription.update(
98
+ processor_id,
99
+ {
100
+ plan: processor_plan,
101
+ trial_end: (on_trial? ? trial_ends_at.to_i : "now"),
102
+ cancel_at_period_end: false
103
+ },
104
+ {stripe_account: stripe_account}
105
+ )
66
106
  rescue ::Stripe::StripeError => e
67
107
  raise Pay::Stripe::Error, e
68
108
  end
69
109
 
70
110
  def swap(plan)
71
- subscription = processor_subscription
72
- subscription.cancel_at_period_end = false
73
- subscription.plan = plan
74
- subscription.proration_behavior = (prorate ? "create_prorations" : "none")
75
- subscription.trial_end = on_trial? ? trial_ends_at.to_i : "now"
76
- subscription.quantity = quantity if quantity?
77
- subscription.save
111
+ ::Stripe::Subscription.update(
112
+ processor_id,
113
+ {
114
+ cancel_at_period_end: false,
115
+ plan: plan,
116
+ proration_behavior: (prorate ? "create_prorations" : "none"),
117
+ trial_end: (on_trial? ? trial_ends_at.to_i : "now"),
118
+ quantity: quantity
119
+ },
120
+ {stripe_account: stripe_account}
121
+ )
78
122
  rescue ::Stripe::StripeError => e
79
123
  raise Pay::Stripe::Error, e
80
124
  end
@@ -0,0 +1,17 @@
1
+ module Pay
2
+ module Stripe
3
+ module Webhooks
4
+ class AccountUpdated
5
+ def call(event)
6
+ object = event.data.object
7
+
8
+ merchant = Pay.find_merchant("stripe_connect_account_id", object.id)
9
+
10
+ return unless merchant.present?
11
+
12
+ merchant.update(onboarding_complete: object.charges_enabled)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -3,13 +3,8 @@ module Pay
3
3
  module Webhooks
4
4
  class ChargeRefunded
5
5
  def call(event)
6
- object = event.data.object
7
- charge = Pay.charge_model.find_by(processor: :stripe, processor_id: object.id)
8
-
9
- return unless charge.present?
10
-
11
- charge.update(amount_refunded: object.amount_refunded)
12
- notify_user(charge.owner, charge)
6
+ pay_charge = Pay::Stripe::Charge.sync(event.data.object.id)
7
+ notify_user(pay_charge.owner, pay_charge) if pay_charge
13
8
  end
14
9
 
15
10
  def notify_user(billable, charge)
@@ -3,14 +3,8 @@ module Pay
3
3
  module Webhooks
4
4
  class ChargeSucceeded
5
5
  def call(event)
6
- object = event.data.object
7
- billable = Pay.find_billable(processor: :stripe, processor_id: object.customer)
8
-
9
- return unless billable.present?
10
- return if billable.charges.where(processor_id: object.id).any?
11
-
12
- charge = Pay::Stripe::Billable.new(billable).save_pay_charge(object)
13
- notify_user(billable, charge)
6
+ pay_charge = Pay::Stripe::Charge.sync(event.data.object.id)
7
+ notify_user(pay_charge.owner, pay_charge) if pay_charge
14
8
  end
15
9
 
16
10
  def notify_user(billable, charge)
@@ -0,0 +1,13 @@
1
+ module Pay
2
+ module Stripe
3
+ module Webhooks
4
+ class CheckoutSessionAsyncPaymentSucceeded
5
+ def call(event)
6
+ if event.data.object.subscription
7
+ Pay::Stripe::Subscription.sync(event.data.object.subscription)
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module Pay
2
+ module Stripe
3
+ module Webhooks
4
+ class CheckoutSessionCompleted
5
+ def call(event)
6
+ if event.data.object.subscription
7
+ Pay::Stripe::Subscription.sync(event.data.object.subscription)
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -4,15 +4,9 @@ module Pay
4
4
  class PaymentIntentSucceeded
5
5
  def call(event)
6
6
  object = event.data.object
7
- billable = Pay.find_billable(processor: :stripe, processor_id: object.customer)
8
-
9
- return unless billable.present?
10
-
11
7
  object.charges.data.each do |charge|
12
- next if billable.charges.where(processor_id: charge.id).any?
13
-
14
- charge = Pay::Stripe::Billable.new(billable).save_pay_charge(charge)
15
- notify_user(billable, charge)
8
+ pay_charge = Pay::Stripe::Charge.sync(charge.id)
9
+ notify_user(pay_charge.owner, pay_charge) if pay_charge
16
10
  end
17
11
  end
18
12
 
@@ -3,41 +3,7 @@ module Pay
3
3
  module Webhooks
4
4
  class SubscriptionCreated
5
5
  def call(event)
6
- object = event.data.object
7
-
8
- # We may already have the subscription in the database, so we can update that record
9
- subscription = Pay.subscription_model.find_by(processor: :stripe, processor_id: object.id)
10
-
11
- # Create the subscription in the database if we don't have it already
12
- if subscription.nil?
13
- # The customer should already be in the database
14
- owner = Pay.find_billable(processor: :stripe, processor_id: object.customer)
15
-
16
- if owner.nil?
17
- Rails.logger.error("[Pay] Unable to find Pay::Billable with processor: :stripe and processor_id: '#{object.customer}'. Searched these models: #{Pay.billable_models.join(", ")}")
18
- return
19
- end
20
-
21
- subscription = Pay.subscription_model.new(name: Pay.default_product_name, owner: owner, processor: :stripe, processor_id: object.id)
22
- end
23
-
24
- subscription.quantity = object.quantity
25
- subscription.status = object.status
26
- subscription.processor_plan = object.plan.id
27
- subscription.trial_ends_at = Time.at(object.trial_end) if object.trial_end.present?
28
-
29
- # If user was on trial, their subscription ends at the end of the trial
30
- subscription.ends_at = if object.cancel_at_period_end && subscription.on_trial?
31
- subscription.trial_ends_at
32
-
33
- # User wasn't on trial, so subscription ends at period end
34
- elsif object.cancel_at_period_end
35
- Time.at(object.current_period_end)
36
-
37
- # Subscription isn't marked to cancel at period end
38
- end
39
-
40
- subscription.save!
6
+ Pay::Stripe::Subscription.sync(event.data.object.id)
41
7
  end
42
8
  end
43
9
  end
@@ -3,15 +3,7 @@ module Pay
3
3
  module Webhooks
4
4
  class SubscriptionDeleted
5
5
  def call(event)
6
- object = event.data.object
7
- subscription = Pay.subscription_model.find_by(processor: :stripe, processor_id: object.id)
8
-
9
- # We couldn't find the subscription for some reason, maybe it's from another service
10
- return if subscription.nil?
11
-
12
- # User canceled subscriptions have an ends_at
13
- # Automatically canceled subscriptions need this value set
14
- subscription.update!(ends_at: Time.at(object.ended_at)) if subscription.ends_at.blank? && object.ended_at.present?
6
+ Pay::Stripe::Subscription.sync(event.data.object.id)
15
7
  end
16
8
  end
17
9
  end
@@ -5,12 +5,10 @@ module Pay
5
5
  def call(event)
6
6
  # Event is of type "invoice" see:
7
7
  # https://stripe.com/docs/api/invoices/object
8
- subscription = Pay.subscription_model.find_by(
9
- processor: :stripe,
10
- processor_id: event.data.object.subscription
11
- )
12
- date = Time.zone.at(event.data.object.next_payment_attempt)
13
- notify_user(subscription.owner, subscription, date) if subscription.present?
8
+ subscription = Pay.subscription_model.find_by(processor: :stripe, processor_id: event.data.object.subscription)
9
+ return unless subscription
10
+
11
+ notify_user(subscription.owner, subscription, Time.zone.at(event.data.object.next_payment_attempt))
14
12
  end
15
13
 
16
14
  def notify_user(billable, subscription, date)
@@ -3,34 +3,7 @@ module Pay
3
3
  module Webhooks
4
4
  class SubscriptionUpdated
5
5
  def call(event)
6
- object = event.data.object
7
- subscription = Pay.subscription_model.find_by(processor: :stripe, processor_id: object.id)
8
-
9
- return if subscription.nil?
10
-
11
- # Delete any subscription attempts that have expired
12
- if object.status == "incomplete_expired"
13
- subscription.destroy
14
- return
15
- end
16
-
17
- subscription.status = object.status
18
- subscription.quantity = object.quantity
19
- subscription.processor_plan = object.plan.id
20
- subscription.trial_ends_at = Time.at(object.trial_end) if object.trial_end.present?
21
-
22
- # If user was on trial, their subscription ends at the end of the trial
23
- subscription.ends_at = if object.cancel_at_period_end && subscription.on_trial?
24
- subscription.trial_ends_at
25
-
26
- # User wasn't on trial, so subscription ends at period end
27
- elsif object.cancel_at_period_end
28
- Time.at(object.current_period_end)
29
-
30
- # Subscription isn't marked to cancel at period end
31
- end
32
-
33
- subscription.save!
6
+ Pay::Stripe::Subscription.sync(event.data.object.id)
34
7
  end
35
8
  end
36
9
  end