pay 2.6.10 → 2.7.2

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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +93 -54
  3. data/app/models/pay/application_record.rb +1 -0
  4. data/app/models/pay/charge.rb +3 -1
  5. data/app/models/pay/subscription.rb +5 -3
  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/db/migrate/20210714175351_add_uniqueness_to_pay_models.rb +6 -0
  11. data/lib/generators/active_record/billable_generator.rb +44 -0
  12. data/lib/generators/active_record/merchant_generator.rb +44 -0
  13. data/lib/generators/active_record/templates/billable_migration.rb +17 -0
  14. data/lib/generators/active_record/templates/merchant_migration.rb +12 -0
  15. data/lib/generators/pay/{pay_generator.rb → billable_generator.rb} +2 -3
  16. data/lib/generators/pay/merchant_generator.rb +17 -0
  17. data/lib/generators/pay/orm_helpers.rb +10 -6
  18. data/lib/pay.rb +22 -0
  19. data/lib/pay/adapter.rb +22 -0
  20. data/lib/pay/billable.rb +4 -0
  21. data/lib/pay/braintree/billable.rb +8 -1
  22. data/lib/pay/braintree/subscription.rb +6 -0
  23. data/lib/pay/env.rb +8 -0
  24. data/lib/pay/fake_processor/subscription.rb +6 -0
  25. data/lib/pay/merchant.rb +37 -0
  26. data/lib/pay/paddle/subscription.rb +9 -0
  27. data/lib/pay/paddle/webhooks/subscription_payment_succeeded.rb +3 -1
  28. data/lib/pay/stripe.rb +11 -0
  29. data/lib/pay/stripe/billable.rb +39 -36
  30. data/lib/pay/stripe/charge.rb +62 -4
  31. data/lib/pay/stripe/merchant.rb +66 -0
  32. data/lib/pay/stripe/subscription.rb +87 -21
  33. data/lib/pay/stripe/webhooks/account_updated.rb +17 -0
  34. data/lib/pay/stripe/webhooks/charge_refunded.rb +2 -7
  35. data/lib/pay/stripe/webhooks/charge_succeeded.rb +2 -8
  36. data/lib/pay/stripe/webhooks/checkout_session_async_payment_succeeded.rb +13 -0
  37. data/lib/pay/stripe/webhooks/checkout_session_completed.rb +13 -0
  38. data/lib/pay/stripe/webhooks/payment_intent_succeeded.rb +2 -8
  39. data/lib/pay/stripe/webhooks/payment_method_attached.rb +17 -0
  40. data/lib/pay/stripe/webhooks/payment_method_automatically_updated.rb +17 -0
  41. data/lib/pay/stripe/webhooks/payment_method_detached.rb +17 -0
  42. data/lib/pay/stripe/webhooks/subscription_created.rb +1 -35
  43. data/lib/pay/stripe/webhooks/subscription_deleted.rb +1 -9
  44. data/lib/pay/stripe/webhooks/subscription_renewing.rb +4 -6
  45. data/lib/pay/stripe/webhooks/subscription_updated.rb +1 -28
  46. data/lib/pay/version.rb +1 -1
  47. metadata +22 -6
  48. data/lib/generators/active_record/pay_generator.rb +0 -58
  49. data/lib/generators/active_record/templates/migration.rb +0 -9
@@ -3,24 +3,82 @@ 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, try: 0, retries: 1)
9
+ # Skip loading the latest charge details from the API if we already have it
10
+ object ||= ::Stripe::Charge.retrieve(id: charge_id)
11
+
12
+ owner = Pay.find_billable(processor: :stripe, processor_id: object.customer)
13
+ return unless owner
14
+
15
+ attrs = {
16
+ amount: object.amount,
17
+ amount_refunded: object.amount_refunded,
18
+ application_fee_amount: object.application_fee_amount,
19
+ card_exp_month: object.payment_method_details.card.exp_month,
20
+ card_exp_year: object.payment_method_details.card.exp_year,
21
+ card_last4: object.payment_method_details.card.last4,
22
+ card_type: object.payment_method_details.card.brand,
23
+ created_at: Time.at(object.created),
24
+ currency: object.currency,
25
+ stripe_account: owner.stripe_account
26
+ }
27
+
28
+ # Associate charge with subscription if we can
29
+ if object.invoice
30
+ invoice = (object.invoice.is_a?(::Stripe::Invoice) ? object.invoice : ::Stripe::Invoice.retrieve(object.invoice))
31
+ attrs[:subscription] = Pay::Subscription.find_by(processor: :stripe, processor_id: invoice.subscription)
32
+ end
33
+
34
+ # Update or create the charge
35
+ processor_details = {processor: :stripe, processor_id: object.id}
36
+ if (pay_charge = owner.charges.find_by(processor_details))
37
+ pay_charge.with_lock do
38
+ pay_charge.update!(attrs)
39
+ end
40
+ pay_charge
41
+ else
42
+ owner.charges.create!(attrs.merge(processor_details))
43
+ end
44
+ rescue ActiveRecord::RecordInvalid
45
+ try += 1
46
+ if try <= retries
47
+ sleep 0.1
48
+ retry
49
+ else
50
+ raise
51
+ end
52
+ end
7
53
 
8
54
  def initialize(pay_charge)
9
55
  @pay_charge = pay_charge
10
56
  end
11
57
 
12
58
  def charge
13
- ::Stripe::Charge.retrieve(id: processor_id, expand: ["customer", "invoice.subscription"])
59
+ ::Stripe::Charge.retrieve({id: processor_id, expand: ["customer", "invoice.subscription"]}, stripe_options)
14
60
  rescue ::Stripe::StripeError => e
15
61
  raise Pay::Stripe::Error, e
16
62
  end
17
63
 
18
- def refund!(amount_to_refund)
19
- ::Stripe::Refund.create(charge: processor_id, amount: amount_to_refund)
64
+ # https://stripe.com/docs/api/refunds/create
65
+ #
66
+ # refund!
67
+ # refund!(5_00)
68
+ # refund!(5_00, refund_application_fee: true)
69
+ def refund!(amount_to_refund, **options)
70
+ ::Stripe::Refund.create(options.merge(charge: processor_id, amount: amount_to_refund), stripe_options)
20
71
  pay_charge.update(amount_refunded: amount_to_refund)
21
72
  rescue ::Stripe::StripeError => e
22
73
  raise Pay::Stripe::Error, e
23
74
  end
75
+
76
+ private
77
+
78
+ # Options for Stripe requests
79
+ def stripe_options
80
+ {stripe_account: stripe_account}.compact
81
+ end
24
82
  end
25
83
  end
26
84
  end
@@ -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,33 +16,82 @@ 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, try: 0, retries: 1)
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
+ attributes[:ends_at] = if object.ended_at
41
+ # Fully cancelled subscription
42
+ Time.at(object.ended_at)
43
+ elsif object.cancel_at
44
+ # subscription cancelling in the future
45
+ Time.at(object.cancel_at)
46
+ elsif object.cancel_at_period_end
47
+ # Subscriptions cancelling in the future
48
+ Time.at(object.current_period_end)
49
+ end
50
+
51
+ # Update or create the subscription
52
+ processor_details = {processor: :stripe, processor_id: object.id}
53
+ if (pay_subscription = owner.subscriptions.find_by(processor_details))
54
+ pay_subscription.with_lock do
55
+ pay_subscription.update!(attributes)
56
+ end
57
+ pay_subscription
58
+ else
59
+ owner.subscriptions.create!(attributes.merge(processor_details))
60
+ end
61
+ rescue ActiveRecord::RecordInvalid
62
+ try += 1
63
+ if try <= retries
64
+ sleep 0.1
65
+ retry
66
+ else
67
+ raise
68
+ end
69
+ end
70
+
22
71
  def initialize(pay_subscription)
23
72
  @pay_subscription = pay_subscription
24
73
  end
25
74
 
26
- def cancel
27
- subscription = processor_subscription
28
- subscription.cancel_at_period_end = true
29
- subscription.save
75
+ def subscription(**options)
76
+ ::Stripe::Subscription.retrieve(options.merge(id: processor_id))
77
+ end
30
78
 
31
- new_ends_at = on_trial? ? trial_ends_at : Time.at(subscription.current_period_end)
32
- pay_subscription.update(ends_at: new_ends_at)
79
+ def cancel
80
+ stripe_sub = ::Stripe::Subscription.update(processor_id, {cancel_at_period_end: true}, stripe_options)
81
+ pay_subscription.update(ends_at: (on_trial? ? trial_ends_at : Time.at(stripe_sub.current_period_end)))
33
82
  rescue ::Stripe::StripeError => e
34
83
  raise Pay::Stripe::Error, e
35
84
  end
36
85
 
37
86
  def cancel_now!
38
- processor_subscription.delete
39
- pay_subscription.update(ends_at: Time.zone.now, status: :canceled)
87
+ ::Stripe::Subscription.delete(processor_id, {}, stripe_options)
88
+ pay_subscription.update(ends_at: Time.current, status: :canceled)
40
89
  rescue ::Stripe::StripeError => e
41
90
  raise Pay::Stripe::Error, e
42
91
  end
43
92
 
44
93
  def change_quantity(quantity)
45
- ::Stripe::Subscription.update(processor_id, quantity: quantity)
94
+ ::Stripe::Subscription.update(processor_id, {quantity: quantity}, stripe_options)
46
95
  rescue ::Stripe::StripeError => e
47
96
  raise Pay::Stripe::Error, e
48
97
  end
@@ -64,26 +113,43 @@ module Pay
64
113
  raise StandardError, "You can only resume subscriptions within their grace period."
65
114
  end
66
115
 
67
- subscription = processor_subscription
68
- subscription.plan = processor_plan
69
- subscription.trial_end = on_trial? ? trial_ends_at.to_i : "now"
70
- subscription.cancel_at_period_end = false
71
- subscription.save
116
+ ::Stripe::Subscription.update(
117
+ processor_id,
118
+ {
119
+ plan: processor_plan,
120
+ trial_end: (on_trial? ? trial_ends_at.to_i : "now"),
121
+ cancel_at_period_end: false
122
+ },
123
+ stripe_options
124
+ )
72
125
  rescue ::Stripe::StripeError => e
73
126
  raise Pay::Stripe::Error, e
74
127
  end
75
128
 
76
129
  def swap(plan)
77
- subscription = processor_subscription
78
- subscription.cancel_at_period_end = false
79
- subscription.plan = plan
80
- subscription.proration_behavior = (prorate ? "create_prorations" : "none")
81
- subscription.trial_end = on_trial? ? trial_ends_at.to_i : "now"
82
- subscription.quantity = quantity if quantity?
83
- subscription.save
130
+ raise ArgumentError, "plan must be a string" unless plan.is_a?(String)
131
+
132
+ ::Stripe::Subscription.update(
133
+ processor_id,
134
+ {
135
+ cancel_at_period_end: false,
136
+ plan: plan,
137
+ proration_behavior: (prorate ? "create_prorations" : "none"),
138
+ trial_end: (on_trial? ? trial_ends_at.to_i : "now"),
139
+ quantity: quantity
140
+ },
141
+ stripe_options
142
+ )
84
143
  rescue ::Stripe::StripeError => e
85
144
  raise Pay::Stripe::Error, e
86
145
  end
146
+
147
+ private
148
+
149
+ # Options for Stripe requests
150
+ def stripe_options
151
+ {stripe_account: stripe_account}.compact
152
+ end
87
153
  end
88
154
  end
89
155
  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
 
@@ -0,0 +1,17 @@
1
+ module Pay
2
+ module Stripe
3
+ module Webhooks
4
+ class PaymentMethodAttached
5
+ def call(event)
6
+ object = event.data.object
7
+ pay_customer = Pay::Customer.find_by(processor: :stripe, processor_id: object.customer)
8
+
9
+ # Couldn't find user, we can skip
10
+ return unless pay_customer.present?
11
+
12
+ Pay::Stripe::Billable.new(pay_customer).sync_payment_method(payment_method_id: object.id)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ module Pay
2
+ module Stripe
3
+ module Webhooks
4
+ class PaymentMethodAutomaticallyUpdated
5
+ def call(event)
6
+ object = event.data.object
7
+ pay_customer = Pay::Customer.find_by(processor: :stripe, processor_id: object.customer)
8
+
9
+ # Couldn't find user, we can skip
10
+ return unless pay_customer.present?
11
+
12
+ Pay::Stripe::Billable.new(pay_customer).sync_payment_method(payment_method_id: object.id)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ module Pay
2
+ module Stripe
3
+ module Webhooks
4
+ class PaymentMethodDetached
5
+ def call(event)
6
+ object = event.data.object
7
+ pay_customer = Pay::Customer.find_by(processor: :stripe, processor_id: object.customer)
8
+
9
+ # Couldn't find user, we can skip
10
+ return unless pay_customer.present?
11
+
12
+ Pay::Stripe::Billable.new(pay_customer).sync_payment_method(payment_method_id: object.id)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end