pay 2.6.11 → 3.0.0

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 (98) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +35 -693
  3. data/app/controllers/pay/webhooks/braintree_controller.rb +10 -3
  4. data/app/controllers/pay/webhooks/paddle_controller.rb +7 -8
  5. data/app/controllers/pay/webhooks/stripe_controller.rb +6 -3
  6. data/app/jobs/pay/{email_sync_job.rb → customer_sync_job.rb} +3 -4
  7. data/app/models/pay/application_record.rb +1 -5
  8. data/app/models/pay/charge.rb +32 -17
  9. data/app/models/pay/customer.rb +87 -0
  10. data/app/models/pay/merchant.rb +19 -0
  11. data/app/models/pay/payment_method.rb +41 -0
  12. data/app/models/pay/subscription.rb +34 -30
  13. data/app/models/pay/webhook.rb +36 -0
  14. data/app/views/layouts/pay/application.html.erb +2 -3
  15. data/app/views/pay/payments/show.html.erb +109 -81
  16. data/app/views/pay/user_mailer/receipt.html.erb +2 -2
  17. data/app/views/pay/user_mailer/refund.html.erb +2 -2
  18. data/config/locales/en.yml +1 -1
  19. data/db/migrate/1_create_pay_tables.rb +72 -0
  20. data/lib/generators/active_record/billable_generator.rb +44 -0
  21. data/lib/generators/active_record/merchant_generator.rb +44 -0
  22. data/lib/generators/active_record/templates/billable_migration.rb +17 -0
  23. data/lib/generators/active_record/templates/merchant_migration.rb +12 -0
  24. data/lib/generators/pay/{pay_generator.rb → billable_generator.rb} +2 -3
  25. data/lib/generators/pay/merchant_generator.rb +17 -0
  26. data/lib/generators/pay/orm_helpers.rb +10 -6
  27. data/lib/pay/adapter.rb +22 -0
  28. data/lib/pay/attributes.rb +74 -0
  29. data/lib/pay/billable/sync_customer.rb +30 -0
  30. data/lib/pay/braintree/billable.rb +130 -105
  31. data/lib/pay/braintree/payment_method.rb +33 -0
  32. data/lib/pay/braintree/subscription.rb +9 -12
  33. data/lib/pay/braintree/webhooks/subscription_canceled.rb +1 -1
  34. data/lib/pay/braintree/webhooks/subscription_charged_successfully.rb +4 -4
  35. data/lib/pay/braintree/webhooks/subscription_charged_unsuccessfully.rb +1 -1
  36. data/lib/pay/braintree/webhooks/subscription_expired.rb +1 -1
  37. data/lib/pay/braintree/webhooks/subscription_trial_ended.rb +2 -2
  38. data/lib/pay/braintree/webhooks/subscription_went_active.rb +1 -1
  39. data/lib/pay/braintree/webhooks/subscription_went_past_due.rb +1 -1
  40. data/lib/pay/braintree.rb +3 -2
  41. data/lib/pay/engine.rb +6 -1
  42. data/lib/pay/env.rb +8 -0
  43. data/lib/pay/fake_processor/billable.rb +45 -21
  44. data/lib/pay/fake_processor/payment_method.rb +21 -0
  45. data/lib/pay/fake_processor/subscription.rb +11 -8
  46. data/lib/pay/fake_processor.rb +2 -1
  47. data/lib/pay/merchant.rb +37 -0
  48. data/lib/pay/nano_id.rb +13 -0
  49. data/lib/pay/paddle/billable.rb +18 -48
  50. data/lib/pay/paddle/charge.rb +5 -5
  51. data/lib/pay/paddle/payment_method.rb +58 -0
  52. data/lib/pay/paddle/response.rb +0 -0
  53. data/lib/pay/paddle/subscription.rb +49 -8
  54. data/lib/pay/paddle/webhooks/subscription_cancelled.rb +6 -3
  55. data/lib/pay/paddle/webhooks/subscription_created.rb +1 -40
  56. data/lib/pay/paddle/webhooks/subscription_payment_refunded.rb +3 -3
  57. data/lib/pay/paddle/webhooks/subscription_payment_succeeded.rb +23 -23
  58. data/lib/pay/paddle/webhooks/subscription_updated.rb +2 -2
  59. data/lib/pay/paddle.rb +7 -3
  60. data/lib/pay/payment.rb +1 -1
  61. data/lib/pay/receipts.rb +35 -7
  62. data/lib/pay/stripe/billable.rb +82 -93
  63. data/lib/pay/stripe/charge.rb +65 -4
  64. data/lib/pay/stripe/merchant.rb +66 -0
  65. data/lib/pay/stripe/payment_method.rb +61 -0
  66. data/lib/pay/stripe/subscription.rb +91 -24
  67. data/lib/pay/stripe/webhooks/account_updated.rb +16 -0
  68. data/lib/pay/stripe/webhooks/charge_refunded.rb +2 -7
  69. data/lib/pay/stripe/webhooks/charge_succeeded.rb +2 -8
  70. data/lib/pay/stripe/webhooks/checkout_session_async_payment_succeeded.rb +15 -0
  71. data/lib/pay/stripe/webhooks/checkout_session_completed.rb +15 -0
  72. data/lib/pay/stripe/webhooks/customer_deleted.rb +7 -15
  73. data/lib/pay/stripe/webhooks/customer_updated.rb +10 -3
  74. data/lib/pay/stripe/webhooks/payment_action_required.rb +2 -2
  75. data/lib/pay/stripe/webhooks/payment_intent_succeeded.rb +6 -14
  76. data/lib/pay/stripe/webhooks/payment_method_attached.rb +15 -0
  77. data/lib/pay/stripe/webhooks/payment_method_detached.rb +12 -0
  78. data/lib/pay/stripe/webhooks/payment_method_updated.rb +10 -4
  79. data/lib/pay/stripe/webhooks/subscription_created.rb +1 -35
  80. data/lib/pay/stripe/webhooks/subscription_deleted.rb +2 -9
  81. data/lib/pay/stripe/webhooks/subscription_renewing.rb +14 -6
  82. data/lib/pay/stripe/webhooks/subscription_updated.rb +1 -28
  83. data/lib/pay/stripe.rb +17 -3
  84. data/lib/pay/version.rb +1 -1
  85. data/lib/pay/webhooks/delegator.rb +4 -0
  86. data/lib/pay/webhooks/process_job.rb +9 -0
  87. data/lib/pay/webhooks.rb +1 -0
  88. data/lib/pay.rb +8 -57
  89. metadata +34 -36
  90. data/db/migrate/20170205020145_create_pay_subscriptions.rb +0 -17
  91. data/db/migrate/20170727235816_create_pay_charges.rb +0 -18
  92. data/db/migrate/20190816015720_add_status_to_pay_subscriptions.rb +0 -14
  93. data/db/migrate/20200603134434_add_data_to_pay_models.rb +0 -22
  94. data/db/migrate/20210423235138_add_currency_to_pay_charges.rb +0 -5
  95. data/lib/generators/active_record/pay_generator.rb +0 -58
  96. data/lib/generators/active_record/templates/migration.rb +0 -9
  97. data/lib/pay/billable/sync_email.rb +0 -40
  98. data/lib/pay/billable.rb +0 -168
@@ -0,0 +1,66 @@
1
+ module Pay
2
+ module Stripe
3
+ class Merchant
4
+ attr_reader :pay_merchant
5
+
6
+ delegate :processor_id,
7
+ to: :pay_merchant
8
+
9
+ def initialize(pay_merchant)
10
+ @pay_merchant = pay_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
+ pay_merchant.update(processor_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(processor_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: processor_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(processor_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: processor_id
60
+ }.merge(options))
61
+ rescue ::Stripe::StripeError => e
62
+ raise Pay::Stripe::Error, e
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,61 @@
1
+ module Pay
2
+ module Stripe
3
+ class PaymentMethod
4
+ attr_reader :pay_payment_method
5
+
6
+ delegate :customer, :processor_id, to: :pay_payment_method
7
+
8
+ def initialize(pay_payment_method)
9
+ @pay_payment_method = pay_payment_method
10
+ end
11
+
12
+ def self.sync(id, object: nil, stripe_account: nil, try: 0, retries: 1)
13
+ object ||= ::Stripe::PaymentMethod.retrieve(id, {stripe_account: stripe_account}.compact)
14
+
15
+ pay_customer = Pay::Customer.find_by(processor: :stripe, processor_id: object.customer)
16
+ return unless pay_customer
17
+
18
+ default_payment_method_id = pay_customer.customer.invoice_settings.default_payment_method
19
+ default = (id == default_payment_method_id)
20
+
21
+ attributes = extract_attributes(object).merge(default: default)
22
+
23
+ pay_customer.payment_methods.update_all(default: false) if default
24
+ pay_payment_method = pay_customer.payment_methods.where(processor_id: object.id).first_or_initialize
25
+ pay_payment_method.update!(attributes)
26
+ pay_payment_method
27
+ end
28
+
29
+ # Extracts payment method details from a Stripe::PaymentMethod object
30
+ def self.extract_attributes(payment_method)
31
+ details = payment_method.send(payment_method.type)
32
+
33
+ {
34
+ payment_method_type: payment_method.type,
35
+ brand: details.try(:brand)&.capitalize,
36
+ last4: details.try(:last4).to_s,
37
+ exp_month: details.try(:exp_month).to_s,
38
+ exp_year: details.try(:exp_year).to_s,
39
+ bank: details.try(:bank_name) || details.try(:bank) # eps, fpx, ideal, p24, acss_debit, etc
40
+ }
41
+ end
42
+
43
+ # Sets payment method as default
44
+ def make_default!
45
+ ::Stripe::Customer.update(customer.processor_id, {invoice_settings: {default_payment_method: processor_id}}, stripe_options)
46
+ end
47
+
48
+ # Remove payment method
49
+ def detach
50
+ ::Stripe::PaymentMethod.detach(processor_id, stripe_options)
51
+ end
52
+
53
+ private
54
+
55
+ # Options for Stripe requests
56
+ def stripe_options
57
+ {stripe_account: customer.stripe_account}.compact
58
+ end
59
+ end
60
+ end
61
+ end
@@ -8,7 +8,6 @@ module Pay
8
8
  :ends_at,
9
9
  :name,
10
10
  :on_trial?,
11
- :owner,
12
11
  :processor_id,
13
12
  :processor_plan,
14
13
  :processor_subscription,
@@ -16,43 +15,94 @@ module Pay
16
15
  :prorate?,
17
16
  :quantity,
18
17
  :quantity?,
18
+ :stripe_account,
19
19
  :trial_ends_at,
20
20
  to: :pay_subscription
21
21
 
22
+ def self.sync(subscription_id, object: nil, name: Pay.default_product_name, stripe_account: nil, try: 0, retries: 1)
23
+ # Skip loading the latest subscription details from the API if we already have it
24
+ object ||= ::Stripe::Subscription.retrieve({id: subscription_id, expand: ["pending_setup_intent", "latest_invoice.payment_intent", "latest_invoice.charge.invoice"]}, {stripe_account: stripe_account}.compact)
25
+
26
+ pay_customer = Pay::Customer.find_by(processor: :stripe, processor_id: object.customer)
27
+ return unless pay_customer
28
+
29
+ attributes = {
30
+ application_fee_percent: object.application_fee_percent,
31
+ processor_plan: object.plan.id,
32
+ quantity: object.quantity,
33
+ status: object.status,
34
+ stripe_account: pay_customer.stripe_account,
35
+ trial_ends_at: (object.trial_end ? Time.at(object.trial_end) : nil),
36
+ metadata: object.metadata
37
+ }
38
+
39
+ attributes[:ends_at] = if object.ended_at
40
+ # Fully cancelled subscription
41
+ Time.at(object.ended_at)
42
+ elsif object.cancel_at
43
+ # subscription cancelling in the future
44
+ Time.at(object.cancel_at)
45
+ elsif object.cancel_at_period_end
46
+ # Subscriptions cancelling in the future
47
+ Time.at(object.current_period_end)
48
+ end
49
+
50
+ # Update or create the subscription
51
+ pay_subscription = pay_customer.subscriptions.find_by(processor_id: object.id)
52
+ if pay_subscription
53
+ pay_subscription.with_lock { pay_subscription.update!(attributes) }
54
+ else
55
+ pay_subscription = pay_customer.subscriptions.create!(attributes.merge(name: name, processor_id: object.id))
56
+ end
57
+
58
+ # Sync the latest charge if we already have it loaded (like during subscrbe), otherwise, let webhooks take care of creating it
59
+ if (charge = object.try(:latest_invoice).try(:charge)) && charge.try(:status) == "succeeded"
60
+ Pay::Stripe::Charge.sync(charge.id, object: charge)
61
+ end
62
+
63
+ pay_subscription
64
+ rescue ActiveRecord::RecordInvalid
65
+ try += 1
66
+ if try <= retries
67
+ sleep 0.1
68
+ retry
69
+ else
70
+ raise
71
+ end
72
+ end
73
+
22
74
  def initialize(pay_subscription)
23
75
  @pay_subscription = pay_subscription
24
76
  end
25
77
 
26
78
  def subscription(**options)
27
- ::Stripe::Subscription.retrieve(options.merge(id: processor_id))
79
+ options[:id] = processor_id
80
+ options[:expand] ||= ["pending_setup_intent", "latest_invoice.payment_intent", "latest_invoice.charge.invoice"]
81
+ ::Stripe::Subscription.retrieve(options, {stripe_account: stripe_account}.compact)
28
82
  end
29
83
 
30
84
  def cancel
31
- subscription = processor_subscription
32
- subscription.cancel_at_period_end = true
33
- subscription.save
34
-
35
- new_ends_at = on_trial? ? trial_ends_at : Time.at(subscription.current_period_end)
36
- pay_subscription.update(ends_at: new_ends_at)
85
+ stripe_sub = ::Stripe::Subscription.update(processor_id, {cancel_at_period_end: true}, stripe_options)
86
+ pay_subscription.update(ends_at: (on_trial? ? trial_ends_at : Time.at(stripe_sub.current_period_end)))
37
87
  rescue ::Stripe::StripeError => e
38
88
  raise Pay::Stripe::Error, e
39
89
  end
40
90
 
41
91
  def cancel_now!
42
- processor_subscription.delete
43
- pay_subscription.update(ends_at: Time.zone.now, status: :canceled)
92
+ ::Stripe::Subscription.delete(processor_id, {}, stripe_options)
93
+ pay_subscription.update(ends_at: Time.current, status: :canceled)
44
94
  rescue ::Stripe::StripeError => e
45
95
  raise Pay::Stripe::Error, e
46
96
  end
47
97
 
48
98
  def change_quantity(quantity)
49
- ::Stripe::Subscription.update(processor_id, quantity: quantity)
99
+ ::Stripe::Subscription.update(processor_id, {quantity: quantity}, stripe_options)
50
100
  rescue ::Stripe::StripeError => e
51
101
  raise Pay::Stripe::Error, e
52
102
  end
53
103
 
54
104
  def on_grace_period?
55
- canceled? && Time.zone.now < ends_at
105
+ canceled? && Time.current < ends_at
56
106
  end
57
107
 
58
108
  def paused?
@@ -68,26 +118,43 @@ module Pay
68
118
  raise StandardError, "You can only resume subscriptions within their grace period."
69
119
  end
70
120
 
71
- subscription = processor_subscription
72
- subscription.plan = processor_plan
73
- subscription.trial_end = on_trial? ? trial_ends_at.to_i : "now"
74
- subscription.cancel_at_period_end = false
75
- subscription.save
121
+ ::Stripe::Subscription.update(
122
+ processor_id,
123
+ {
124
+ plan: processor_plan,
125
+ trial_end: (on_trial? ? trial_ends_at.to_i : "now"),
126
+ cancel_at_period_end: false
127
+ },
128
+ stripe_options
129
+ )
76
130
  rescue ::Stripe::StripeError => e
77
131
  raise Pay::Stripe::Error, e
78
132
  end
79
133
 
80
134
  def swap(plan)
81
- subscription = processor_subscription
82
- subscription.cancel_at_period_end = false
83
- subscription.plan = plan
84
- subscription.proration_behavior = (prorate ? "create_prorations" : "none")
85
- subscription.trial_end = on_trial? ? trial_ends_at.to_i : "now"
86
- subscription.quantity = quantity if quantity?
87
- subscription.save
135
+ raise ArgumentError, "plan must be a string" unless plan.is_a?(String)
136
+
137
+ ::Stripe::Subscription.update(
138
+ processor_id,
139
+ {
140
+ cancel_at_period_end: false,
141
+ plan: plan,
142
+ proration_behavior: (prorate ? "create_prorations" : "none"),
143
+ trial_end: (on_trial? ? trial_ends_at.to_i : "now"),
144
+ quantity: quantity
145
+ },
146
+ stripe_options
147
+ )
88
148
  rescue ::Stripe::StripeError => e
89
149
  raise Pay::Stripe::Error, e
90
150
  end
151
+
152
+ private
153
+
154
+ # Options for Stripe requests
155
+ def stripe_options
156
+ {stripe_account: stripe_account}.compact
157
+ end
91
158
  end
92
159
  end
93
160
  end
@@ -0,0 +1,16 @@
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::Merchant.find_by(processor: :stripe, processor_id: object.id)
9
+ return unless merchant
10
+
11
+ merchant.update(onboarding_complete: object.charges_enabled)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ 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, stripe_account: event.try(:account))
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, stripe_account: event.try(:account))
7
+ notify_user(pay_charge.customer.owner, pay_charge) if pay_charge
14
8
  end
15
9
 
16
10
  def notify_user(billable, charge)
@@ -0,0 +1,15 @@
1
+ module Pay
2
+ module Stripe
3
+ module Webhooks
4
+ class CheckoutSessionAsyncPaymentSucceeded
5
+ def call(event)
6
+ # TODO: Also handle payment intents
7
+
8
+ if event.data.object.subscription
9
+ Pay::Stripe::Subscription.sync(event.data.object.subscription, stripe_account: event.try(:account))
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module Pay
2
+ module Stripe
3
+ module Webhooks
4
+ class CheckoutSessionCompleted
5
+ def call(event)
6
+ # TODO: Also handle payment intents
7
+
8
+ if event.data.object.subscription
9
+ Pay::Stripe::Subscription.sync(event.data.object.subscription, stripe_account: event.try(:account))
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -4,24 +4,16 @@ module Pay
4
4
  class CustomerDeleted
5
5
  def call(event)
6
6
  object = event.data.object
7
- billable = Pay.find_billable(processor: :stripe, processor_id: object.id)
7
+ pay_customer = Pay::Customer.find_by(processor: :stripe, processor_id: object.id)
8
8
 
9
- # Couldn't find user, we can skip
10
- return unless billable.present?
9
+ # Mark all subscriptions as canceled
10
+ pay_customer.subscriptions.active.update_all(ends_at: Time.current, status: "canceled")
11
11
 
12
- billable.update(
13
- processor_id: nil,
14
- trial_ends_at: nil,
15
- card_type: nil,
16
- card_last4: nil,
17
- card_exp_month: nil,
18
- card_exp_year: nil
19
- )
12
+ # Remove all payment methods
13
+ pay_customer.payment_methods.destroy_all
20
14
 
21
- billable.subscriptions.update_all(
22
- trial_ends_at: nil,
23
- ends_at: Time.zone.now
24
- )
15
+ # Mark customer as deleted
16
+ pay_customer&.update!(default: false, deleted_at: Time.current)
25
17
  end
26
18
  end
27
19
  end
@@ -4,12 +4,19 @@ module Pay
4
4
  class CustomerUpdated
5
5
  def call(event)
6
6
  object = event.data.object
7
- billable = Pay.find_billable(processor: :stripe, processor_id: object.id)
7
+ pay_customer = Pay::Customer.find_by(processor: :stripe, processor_id: object.id)
8
8
 
9
9
  # Couldn't find user, we can skip
10
- return unless billable.present?
10
+ return unless pay_customer.present?
11
11
 
12
- Pay::Stripe::Billable.new(billable).sync_card_from_stripe
12
+ # Sync default card
13
+ if (payment_method_id = pay_customer.customer.invoice_settings.default_payment_method)
14
+ Pay::Stripe::PaymentMethod.sync(payment_method_id, {stripe_account: event.account}.compact)
15
+
16
+ else
17
+ # No default payment method set
18
+ pay_customer.payment_methods.update_all(default: false)
19
+ end
13
20
  end
14
21
  end
15
22
  end
@@ -8,9 +8,9 @@ module Pay
8
8
 
9
9
  object = event.data.object
10
10
 
11
- subscription = Pay.subscription_model.find_by(processor: :stripe, processor_id: object.subscription)
11
+ subscription = Pay::Subscription.find_by_processor_and_id(:stripe, object.subscription)
12
12
  return if subscription.nil?
13
- billable = subscription.owner
13
+ billable = subscription.customer.owner
14
14
 
15
15
  notify_user(billable, event.data.object.payment_intent, subscription)
16
16
  end