pay 7.3.0 → 11.2.2

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 (89) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -4
  3. data/app/controllers/pay/payments_controller.rb +2 -0
  4. data/app/controllers/pay/webhooks/lemon_squeezy_controller.rb +45 -0
  5. data/app/jobs/pay/customer_sync_job.rb +1 -1
  6. data/app/models/concerns/pay/routing.rb +13 -0
  7. data/{lib → app/models}/pay/braintree/charge.rb +7 -12
  8. data/{lib/pay/braintree/billable.rb → app/models/pay/braintree/customer.rb} +33 -71
  9. data/{lib → app/models}/pay/braintree/payment_method.rb +4 -10
  10. data/{lib → app/models}/pay/braintree/subscription.rb +23 -61
  11. data/app/models/pay/charge.rb +16 -45
  12. data/app/models/pay/customer.rb +5 -16
  13. data/app/models/pay/fake_processor/charge.rb +19 -0
  14. data/{lib/pay/fake_processor/billable.rb → app/models/pay/fake_processor/customer.rb} +28 -38
  15. data/{lib → app/models}/pay/fake_processor/merchant.rb +4 -9
  16. data/app/models/pay/fake_processor/payment_method.rb +13 -0
  17. data/app/models/pay/fake_processor/subscription.rb +70 -0
  18. data/app/models/pay/lemon_squeezy/charge.rb +96 -0
  19. data/app/models/pay/lemon_squeezy/customer.rb +80 -0
  20. data/app/models/pay/lemon_squeezy/payment_method.rb +29 -0
  21. data/app/models/pay/lemon_squeezy/subscription.rb +129 -0
  22. data/app/models/pay/merchant.rb +2 -11
  23. data/{lib → app/models}/pay/paddle_billing/charge.rb +15 -13
  24. data/{lib/pay/paddle_billing/billable.rb → app/models/pay/paddle_billing/customer.rb} +20 -35
  25. data/{lib → app/models}/pay/paddle_billing/payment_method.rb +13 -13
  26. data/{lib → app/models}/pay/paddle_billing/subscription.rb +40 -43
  27. data/{lib → app/models}/pay/paddle_classic/charge.rb +15 -18
  28. data/{lib/pay/paddle_classic/billable.rb → app/models/pay/paddle_classic/customer.rb} +11 -31
  29. data/{lib → app/models}/pay/paddle_classic/payment_method.rb +3 -11
  30. data/{lib → app/models}/pay/paddle_classic/subscription.rb +17 -37
  31. data/app/models/pay/payment_method.rb +4 -5
  32. data/app/models/pay/stripe/charge.rb +155 -0
  33. data/{lib/pay/stripe/billable.rb → app/models/pay/stripe/customer.rb} +78 -111
  34. data/{lib → app/models}/pay/stripe/merchant.rb +5 -20
  35. data/{lib → app/models}/pay/stripe/payment_method.rb +11 -17
  36. data/{lib → app/models}/pay/stripe/subscription.rb +83 -112
  37. data/app/models/pay/subscription.rb +13 -47
  38. data/app/models/pay/webhook.rb +5 -1
  39. data/app/views/pay/user_mailer/payment_action_required.text.erb +9 -0
  40. data/app/views/pay/user_mailer/payment_failed.text.erb +9 -0
  41. data/app/views/pay/user_mailer/receipt.text.erb +20 -0
  42. data/app/views/pay/user_mailer/refund.text.erb +21 -0
  43. data/app/views/pay/user_mailer/subscription_renewing.text.erb +8 -0
  44. data/app/views/pay/user_mailer/subscription_trial_ended.text.erb +8 -0
  45. data/app/views/pay/user_mailer/subscription_trial_will_end.text.erb +8 -0
  46. data/config/locales/en.yml +1 -0
  47. data/config/routes.rb +1 -0
  48. data/db/migrate/20250415151129_add_object_to_pay_models.rb +7 -0
  49. data/db/migrate/2_add_pay_sti_columns.rb +24 -0
  50. data/lib/pay/attributes.rb +16 -8
  51. data/lib/pay/braintree.rb +25 -6
  52. data/lib/pay/engine.rb +2 -0
  53. data/lib/pay/fake_processor.rb +2 -6
  54. data/lib/pay/lemon_squeezy/webhooks/order.rb +11 -0
  55. data/lib/pay/lemon_squeezy/webhooks/subscription.rb +3 -3
  56. data/lib/pay/lemon_squeezy/webhooks/subscription_payment.rb +11 -0
  57. data/lib/pay/lemon_squeezy.rb +58 -104
  58. data/lib/pay/nano_id.rb +1 -1
  59. data/lib/pay/paddle_billing.rb +15 -6
  60. data/lib/pay/paddle_classic/webhooks/signature_verifier.rb +1 -1
  61. data/lib/pay/paddle_classic.rb +11 -9
  62. data/lib/pay/receipts.rb +45 -44
  63. data/lib/pay/stripe/webhooks/charge_updated.rb +11 -0
  64. data/lib/pay/stripe/webhooks/customer_updated.rb +13 -9
  65. data/lib/pay/stripe/webhooks/payment_action_required.rb +10 -6
  66. data/lib/pay/stripe/webhooks/payment_failed.rb +6 -4
  67. data/lib/pay/stripe/webhooks/subscription_renewing.rb +9 -4
  68. data/lib/pay/stripe.rb +28 -9
  69. data/lib/pay/version.rb +1 -1
  70. data/lib/pay.rb +19 -1
  71. data/lib/tasks/pay.rake +2 -2
  72. metadata +45 -43
  73. data/app/views/pay/stripe/_checkout_button.html.erb +0 -21
  74. data/lib/pay/braintree/authorization_error.rb +0 -9
  75. data/lib/pay/braintree/error.rb +0 -23
  76. data/lib/pay/fake_processor/charge.rb +0 -21
  77. data/lib/pay/fake_processor/error.rb +0 -6
  78. data/lib/pay/fake_processor/payment_method.rb +0 -21
  79. data/lib/pay/fake_processor/subscription.rb +0 -90
  80. data/lib/pay/lemon_squeezy/billable.rb +0 -90
  81. data/lib/pay/lemon_squeezy/charge.rb +0 -68
  82. data/lib/pay/lemon_squeezy/error.rb +0 -7
  83. data/lib/pay/lemon_squeezy/payment_method.rb +0 -40
  84. data/lib/pay/lemon_squeezy/subscription.rb +0 -185
  85. data/lib/pay/lemon_squeezy/webhooks/transaction_completed.rb +0 -11
  86. data/lib/pay/paddle_billing/error.rb +0 -7
  87. data/lib/pay/paddle_classic/error.rb +0 -7
  88. data/lib/pay/stripe/charge.rb +0 -176
  89. data/lib/pay/stripe/error.rb +0 -7
@@ -1,26 +1,8 @@
1
1
  module Pay
2
2
  module PaddleClassic
3
- class Subscription
4
- attr_reader :pay_subscription
5
-
6
- delegate :active?,
7
- :canceled?,
8
- :on_grace_period?,
9
- :on_trial?,
10
- :ends_at,
11
- :name,
12
- :owner,
13
- :pause_starts_at,
14
- :pause_starts_at?,
15
- :processor_id,
16
- :processor_plan,
17
- :processor_subscription,
18
- :prorate,
19
- :prorate?,
20
- :quantity,
21
- :quantity?,
22
- :trial_ends_at,
23
- to: :pay_subscription
3
+ class Subscription < Pay::Subscription
4
+ store_accessor :data, :paddle_update_url
5
+ store_accessor :data, :paddle_cancel_url
24
6
 
25
7
  def self.sync(subscription_id, object: nil, name: Pay.default_product_name)
26
8
  # Passthrough is not return from this API, so we can't use that
@@ -68,11 +50,7 @@ module Pay
68
50
  end
69
51
  end
70
52
 
71
- def initialize(pay_subscription)
72
- @pay_subscription = pay_subscription
73
- end
74
-
75
- def subscription(**options)
53
+ def api_record(**options)
76
54
  PaddleClassic.client.users.list(subscription_id: processor_id).data.try(:first)
77
55
  rescue ::Paddle::Error => e
78
56
  raise Pay::PaddleClassic::Error, e
@@ -87,17 +65,17 @@ module Pay
87
65
  elsif paused?
88
66
  pause_starts_at
89
67
  else
90
- Time.parse(processor_subscription.next_payment.date)
68
+ Time.parse(api_record.next_payment.date)
91
69
  end
92
70
 
93
71
  PaddleClassic.client.users.cancel(subscription_id: processor_id)
94
- pay_subscription.update(
72
+ update(
95
73
  status: (ends_at.future? ? :active : :canceled),
96
74
  ends_at: ends_at
97
75
  )
98
76
 
99
77
  # Remove payment methods since customer cannot be reused after cancelling
100
- Pay::PaymentMethod.where(customer_id: pay_subscription.customer_id).destroy_all
78
+ Pay::PaymentMethod.where(customer_id: customer_id).destroy_all
101
79
  rescue ::Paddle::Error => e
102
80
  raise Pay::PaddleClassic::Error, e
103
81
  end
@@ -106,10 +84,10 @@ module Pay
106
84
  return if canceled?
107
85
 
108
86
  PaddleClassic.client.users.cancel(subscription_id: processor_id)
109
- pay_subscription.update(status: :canceled, ends_at: Time.current)
87
+ update(status: :canceled, ends_at: Time.current)
110
88
 
111
89
  # Remove payment methods since customer cannot be reused after cancelling
112
- Pay::PaymentMethod.where(customer_id: pay_subscription.customer_id).destroy_all
90
+ Pay::PaymentMethod.where(customer_id: customer_id).destroy_all
113
91
  rescue ::Paddle::Error => e
114
92
  raise Pay::PaddleClassic::Error, e
115
93
  end
@@ -125,12 +103,12 @@ module Pay
125
103
  end
126
104
 
127
105
  def paused?
128
- pay_subscription.status == "paused"
106
+ status == "paused"
129
107
  end
130
108
 
131
109
  def pause
132
110
  response = PaddleClassic.client.users.pause(subscription_id: processor_id)
133
- pay_subscription.update(status: :paused, pause_starts_at: Time.zone.parse(response.dig(:next_payment, :date)))
111
+ update(status: :paused, pause_starts_at: Time.zone.parse(response.dig(:next_payment, :date)))
134
112
  rescue ::Paddle::Error => e
135
113
  raise Pay::PaddleClassic::Error, e
136
114
  end
@@ -141,11 +119,11 @@ module Pay
141
119
 
142
120
  def resume
143
121
  unless resumable?
144
- raise StandardError, "You can only resume paused subscriptions."
122
+ raise Error, "You can only resume paused subscriptions."
145
123
  end
146
124
 
147
125
  PaddleClassic.client.users.unpause(subscription_id: processor_id)
148
- pay_subscription.update(status: :active, pause_starts_at: nil)
126
+ update(ends_at: nil, status: :active, pause_starts_at: nil)
149
127
  rescue ::Paddle::Error => e
150
128
  raise Pay::PaddleClassic::Error, e
151
129
  end
@@ -153,11 +131,11 @@ module Pay
153
131
  def swap(plan, **options)
154
132
  raise ArgumentError, "plan must be a string" unless plan.is_a?(String)
155
133
 
156
- attributes = {plan_id: plan, prorate: prorate}
134
+ attributes = {plan_id: plan, prorate: options.fetch(:prorate) { true }}
157
135
  attributes[:quantity] = quantity if quantity?
158
136
  PaddleClassic.client.users.update(subscription_id: processor_id, **attributes)
159
137
 
160
- pay_subscription.update(processor_plan: plan, ends_at: nil, status: :active)
138
+ update(processor_plan: plan, ends_at: nil, status: :active)
161
139
  rescue ::Paddle::Error => e
162
140
  raise Pay::PaddleClassic::Error, e
163
141
  end
@@ -168,3 +146,5 @@ module Pay
168
146
  end
169
147
  end
170
148
  end
149
+
150
+ ActiveSupport.run_load_hooks :pay_paddle_classic_subscription, Pay::PaddleClassic::Subscription
@@ -1,9 +1,9 @@
1
1
  module Pay
2
2
  class PaymentMethod < Pay::ApplicationRecord
3
- self.inheritance_column = nil
4
-
5
3
  belongs_to :customer
6
4
 
5
+ delegate :owner, to: :customer
6
+
7
7
  store_accessor :data, :brand # Visa, Mastercard, Discover, PayPal
8
8
  store_accessor :data, :last4
9
9
  store_accessor :data, :exp_month
@@ -12,9 +12,6 @@ module Pay
12
12
  store_accessor :data, :username
13
13
  store_accessor :data, :bank
14
14
 
15
- # Aliases to share PaymentMethodAttributes
16
- alias_attribute :payment_method_type, :type
17
-
18
15
  validates :processor_id, presence: true, uniqueness: {scope: :customer_id, case_sensitive: true}
19
16
 
20
17
  def self.find_by_processor_and_id(processor, processor_id)
@@ -39,3 +36,5 @@ module Pay
39
36
  end
40
37
  end
41
38
  end
39
+
40
+ ActiveSupport.run_load_hooks :pay_payment_method, Pay::PaymentMethod
@@ -0,0 +1,155 @@
1
+ module Pay
2
+ module Stripe
3
+ class Charge < Pay::Charge
4
+ EXPAND = ["balance_transaction", "payment_intent", "refunds.data.balance_transaction"]
5
+
6
+ delegate :amount_captured, :payment_intent, to: :stripe_object, allow_nil: true
7
+
8
+ store_accessor :data, :stripe_invoice
9
+ store_accessor :data, :stripe_receipt_url
10
+
11
+ def self.sync_payment_intent(id, stripe_account: nil)
12
+ payment_intent = ::Stripe::PaymentIntent.retrieve({id: id}, {stripe_account: stripe_account}.compact)
13
+ sync(payment_intent.latest_charge, stripe_account: stripe_account)
14
+ end
15
+
16
+ def self.sync(charge_id, object: nil, stripe_account: nil, try: 0, retries: 1)
17
+ # Skip loading the latest charge details from the API if we already have it
18
+ object ||= ::Stripe::Charge.retrieve({id: charge_id, expand: EXPAND}, {stripe_account: stripe_account}.compact)
19
+ if object.customer.blank?
20
+ Rails.logger.debug "Stripe Charge #{object.id} does not have a customer"
21
+ return
22
+ end
23
+
24
+ pay_customer = Pay::Customer.find_by(processor: :stripe, processor_id: object.customer)
25
+ if pay_customer.blank?
26
+ Rails.logger.debug "Pay::Customer #{object.customer} is not in the database while syncing Stripe Charge #{object.id}"
27
+ return
28
+ end
29
+
30
+ payment_method = object.payment_method_details.try(object.payment_method_details.type)
31
+ attrs = {
32
+ object: object.to_hash,
33
+ amount: object.amount,
34
+ amount_refunded: object.amount_refunded,
35
+ application_fee_amount: object.application_fee_amount,
36
+ bank: payment_method.try(:bank_name) || payment_method.try(:bank), # eps, fpx, ideal, p24, acss_debit, etc
37
+ brand: payment_method.try(:brand)&.capitalize,
38
+ created_at: Time.at(object.created),
39
+ currency: object.currency,
40
+ exp_month: payment_method.try(:exp_month).to_s,
41
+ exp_year: payment_method.try(:exp_year).to_s,
42
+ last4: payment_method.try(:last4).to_s,
43
+ metadata: object.metadata,
44
+ payment_method_type: object.payment_method_details.type,
45
+ stripe_account: pay_customer.stripe_account,
46
+ stripe_receipt_url: object.receipt_url
47
+ }
48
+
49
+ # Associate charge with subscription if we can
50
+ if object.payment_intent.present?
51
+ invoice_payments = ::Stripe::InvoicePayment.list({payment: {type: :payment_intent, payment_intent: object.payment_intent}, status: :paid, expand: ["data.invoice.total_discount_amounts.discount"]}, {stripe_account: stripe_account}.compact)
52
+ if invoice_payments.any? && (invoice = invoice_payments.first.invoice)
53
+ attrs[:stripe_invoice] = invoice.to_hash
54
+ attrs[:subtotal] = invoice.subtotal
55
+ attrs[:tax] = invoice.total - invoice.total_excluding_tax.to_i
56
+ if (subscription = invoice.parent.try(:subscription_details).try(:subscription))
57
+ attrs[:subscription] = pay_customer.subscriptions.find_by(processor_id: subscription)
58
+ end
59
+ end
60
+ end
61
+
62
+ # Update or create the charge
63
+ if (pay_charge = find_by(customer: pay_customer, processor_id: object.id))
64
+ pay_charge.with_lock { pay_charge.update!(attrs) }
65
+ pay_charge
66
+ else
67
+ create!(attrs.merge(customer: pay_customer, processor_id: object.id))
68
+ end
69
+ rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
70
+ if try > retries
71
+ raise
72
+ else
73
+ try += 1
74
+ sleep 0.15**try
75
+ retry
76
+ end
77
+ end
78
+
79
+ def api_record
80
+ ::Stripe::Charge.retrieve({id: processor_id, expand: EXPAND}, stripe_options)
81
+ rescue ::Stripe::StripeError => e
82
+ raise Pay::Stripe::Error, e
83
+ end
84
+
85
+ # Issues a CreditNote if there's an invoice, otherwise uses a Refund
86
+ # This allows Tax to be handled properly
87
+ #
88
+ # https://stripe.com/docs/api/credit_notes/create
89
+ # https://stripe.com/docs/api/refunds/create
90
+ #
91
+ # refund!
92
+ # refund!(5_00)
93
+ # refund!(5_00, refund_application_fee: true)
94
+ def refund!(amount_to_refund = nil, **options)
95
+ amount_to_refund ||= amount
96
+
97
+ if stripe_invoice.present?
98
+ description = options.delete(:description) || I18n.t("pay.refund")
99
+ lines = [{type: :custom_line_item, description: description, quantity: 1, unit_amount: amount_to_refund}]
100
+ credit_note!(**options.merge(refund_amount: amount_to_refund, lines: lines))
101
+ else
102
+ ::Stripe::Refund.create(options.merge(charge: processor_id, amount: amount_to_refund), stripe_options)
103
+ end
104
+ update!(amount_refunded: amount_refunded + amount_to_refund)
105
+ rescue ::Stripe::StripeError => e
106
+ raise Pay::Stripe::Error, e
107
+ end
108
+
109
+ # Adds a credit note to a Stripe Invoice
110
+ def credit_note!(**options)
111
+ raise Pay::Stripe::Error, "no Stripe Invoice on Pay::Charge" if stripe_invoice.blank?
112
+
113
+ ::Stripe::CreditNote.create({invoice: stripe_invoice.id}.merge(options), stripe_options)
114
+ rescue ::Stripe::StripeError => e
115
+ raise Pay::Stripe::Error, e
116
+ end
117
+
118
+ # https://stripe.com/docs/payments/capture-later
119
+ #
120
+ # capture
121
+ # capture(amount_to_capture: 15_00)
122
+ def capture(**options)
123
+ raise Pay::Stripe::Error, "no payment_intent on charge" unless payment_intent.present?
124
+ payment_intent_id = payment_intent.is_a?(::Stripe::PaymentIntent) ? payment_intent.id : payment_intent
125
+ ::Stripe::PaymentIntent.capture(payment_intent_id, options, stripe_options)
126
+ self.class.sync(processor_id)
127
+ rescue ::Stripe::StripeError => e
128
+ raise Pay::Stripe::Error, e
129
+ end
130
+
131
+ def captured?
132
+ amount_captured > 0
133
+ end
134
+
135
+ def stripe_invoice
136
+ if (value = data.dig("stripe_invoice"))
137
+ ::Stripe::Invoice.construct_from(value)
138
+ end
139
+ end
140
+
141
+ def stripe_object
142
+ ::Stripe::Charge.construct_from(object) if object?
143
+ end
144
+
145
+ private
146
+
147
+ # Options for Stripe requests
148
+ def stripe_options
149
+ {stripe_account: stripe_account}.compact
150
+ end
151
+ end
152
+ end
153
+ end
154
+
155
+ ActiveSupport.run_load_hooks :pay_stripe_charge, Pay::Stripe::Charge
@@ -1,36 +1,20 @@
1
1
  module Pay
2
2
  module Stripe
3
- class Billable
4
- include Rails.application.routes.url_helpers
5
-
6
- attr_reader :pay_customer
7
-
8
- delegate :processor_id,
9
- :processor_id?,
10
- :email,
11
- :customer_name,
12
- :payment_method_token,
13
- :payment_method_token?,
14
- :stripe_account,
15
- to: :pay_customer
16
-
17
- def self.default_url_options
18
- Rails.application.config.action_mailer.default_url_options || {}
19
- end
3
+ class Customer < Pay::Customer
4
+ include Pay::Routing
20
5
 
21
- def initialize(pay_customer)
22
- @pay_customer = pay_customer
23
- end
6
+ has_many :charges, dependent: :destroy, class_name: "Pay::Stripe::Charge"
7
+ has_many :subscriptions, dependent: :destroy, class_name: "Pay::Stripe::Subscription"
8
+ has_many :payment_methods, dependent: :destroy, class_name: "Pay::Stripe::PaymentMethod"
9
+ has_one :default_payment_method, -> { where(default: true) }, class_name: "Pay::Stripe::PaymentMethod"
24
10
 
25
11
  # Returns a hash of attributes for the Stripe::Customer object
26
- def customer_attributes
27
- owner = pay_customer.owner
28
-
12
+ def api_record_attributes
29
13
  attributes = case owner.class.pay_stripe_customer_attributes
30
14
  when Symbol
31
- owner.send(owner.class.pay_stripe_customer_attributes, pay_customer)
15
+ owner.send(owner.class.pay_stripe_customer_attributes, self)
32
16
  when Proc
33
- owner.class.pay_stripe_customer_attributes.call(pay_customer)
17
+ owner.class.pay_stripe_customer_attributes.call(self)
34
18
  end
35
19
 
36
20
  # Guard against attributes being returned nil
@@ -39,55 +23,30 @@ module Pay
39
23
  {email: email, name: customer_name}.merge(attributes)
40
24
  end
41
25
 
42
- # Retrieves a Stripe::Customer object
43
- #
44
- # Finds an existing Stripe::Customer if processor_id exists
45
- # Creates a new Stripe::Customer using `customer_attributes` if empty processor_id
46
- #
47
- # Updates the default payment method automatically if a payment_method_token is set
48
- #
49
- # Returns a Stripe::Customer object
50
- def customer
51
- stripe_customer = if processor_id?
52
- ::Stripe::Customer.retrieve({id: processor_id, expand: ["tax", "invoice_credit_balance"]}, stripe_options)
53
- else
54
- sc = ::Stripe::Customer.create(customer_attributes.merge(expand: ["tax"]), stripe_options)
55
- pay_customer.update!(processor_id: sc.id, stripe_account: stripe_account)
56
- sc
57
- end
58
-
59
- if payment_method_token?
60
- add_payment_method(payment_method_token, default: true)
61
- pay_customer.payment_method_token = nil
26
+ def api_record(expand: ["tax", "invoice_credit_balance"])
27
+ with_lock do
28
+ if processor_id?
29
+ ::Stripe::Customer.retrieve({id: processor_id, expand: expand}, stripe_options)
30
+ else
31
+ ::Stripe::Customer.create(api_record_attributes.merge(expand: expand), stripe_options).tap do |customer|
32
+ update!(processor_id: customer.id, stripe_account: stripe_account)
33
+ end
34
+ end
62
35
  end
63
-
64
- stripe_customer
65
36
  rescue ::Stripe::StripeError => e
66
37
  raise Pay::Stripe::Error, e
67
38
  end
68
39
 
69
- # Syncs name and email to Stripe::Customer
70
- # You can also pass in other attributes that will be merged into the default attributes
71
- def update_customer!(**attributes)
72
- customer unless processor_id?
73
- ::Stripe::Customer.update(
74
- processor_id,
75
- customer_attributes.merge(attributes),
76
- stripe_options
77
- )
40
+ def update_api_record(**attributes)
41
+ api_record unless processor_id?
42
+ ::Stripe::Customer.update(processor_id, api_record_attributes.merge(attributes), stripe_options)
78
43
  end
79
44
 
80
45
  # Charges an amount to the customer's default payment method
81
46
  def charge(amount, options = {})
82
- add_payment_method(payment_method_token, default: true) if payment_method_token?
83
-
84
- payment_method = pay_customer.default_payment_method
85
- args = {
86
- confirm: true,
87
- payment_method: payment_method&.processor_id
88
- }.merge(options)
89
-
47
+ args = {confirm: true, payment_method: default_payment_method&.processor_id}.merge(options)
90
48
  payment_intent = create_payment_intent(amount, args)
49
+
91
50
  Pay::Payment.new(payment_intent).validate
92
51
 
93
52
  charge = payment_intent.latest_charge
@@ -96,33 +55,14 @@ module Pay
96
55
  raise Pay::Stripe::Error, e
97
56
  end
98
57
 
99
- # Creates and returns a Stripe::PaymentIntent
100
- def create_payment_intent(amount, options = {})
101
- args = {
102
- amount: amount,
103
- currency: "usd",
104
- customer: processor_id,
105
- expand: ["latest_charge.refunds"],
106
- return_url: root_url
107
- }.merge(options)
108
-
109
- ::Stripe::PaymentIntent.create(args, stripe_options)
110
- end
111
-
112
- # Used for creating Stripe Terminal charges
113
- def terminal_charge(amount, options = {})
114
- create_payment_intent(amount, options.merge(payment_method_types: ["card_present"], capture_method: "manual"))
115
- end
116
-
117
58
  def subscribe(name: Pay.default_product_name, plan: Pay.default_plan_name, **options)
118
59
  quantity = options.delete(:quantity)
119
60
  opts = {
120
- expand: ["pending_setup_intent", "latest_invoice.payment_intent", "latest_invoice.charge"],
121
- items: [plan: plan, quantity: quantity]
61
+ items: [price: plan, quantity: quantity]
122
62
  }.merge(options)
123
63
 
124
64
  # Load the Stripe customer to verify it exists and update payment method if needed
125
- opts[:customer] = customer.id
65
+ opts[:customer] = processor_id || api_record.id
126
66
 
127
67
  # Create subscription on Stripe
128
68
  stripe_sub = ::Stripe::Subscription.create(opts.merge(Pay::Stripe::Subscription.expand_options), stripe_options)
@@ -132,7 +72,8 @@ module Pay
132
72
 
133
73
  # No trial, payment method requires SCA
134
74
  if options[:payment_behavior].to_s != "default_incomplete" && subscription.incomplete?
135
- Pay::Payment.new(stripe_sub.latest_invoice.payment_intent).validate
75
+ payment_intent_id = stripe_sub.latest_invoice.payments.first.payment.payment_intent
76
+ Pay::Payment.from_id(payment_intent_id).validate
136
77
  end
137
78
 
138
79
  subscription
@@ -141,7 +82,7 @@ module Pay
141
82
  end
142
83
 
143
84
  def add_payment_method(payment_method_id, default: false)
144
- customer unless processor_id?
85
+ api_record unless processor_id?
145
86
  payment_method = ::Stripe::PaymentMethod.attach(payment_method_id, {customer: processor_id}, stripe_options)
146
87
 
147
88
  if default
@@ -159,50 +100,56 @@ module Pay
159
100
 
160
101
  # Save the Stripe::PaymentMethod to the database
161
102
  def save_payment_method(payment_method, default:)
162
- pay_payment_method = pay_customer.payment_methods.where(processor_id: payment_method.id).first_or_initialize
103
+ pay_payment_method = payment_methods.where(processor_id: payment_method.id).first_or_initialize
163
104
 
164
105
  attributes = Pay::Stripe::PaymentMethod.extract_attributes(payment_method).merge(default: default)
165
106
 
166
107
  # Ignore the payment method if it's already in the database
167
- pay_customer.payment_methods.where.not(id: pay_payment_method.id).update_all(default: false) if default
108
+ payment_methods.where.not(id: pay_payment_method.id).update_all(default: false) if default
168
109
  pay_payment_method.update!(attributes)
169
110
 
170
111
  # Reload the Rails association
171
- pay_customer.reload_default_payment_method
112
+ reload_default_payment_method
172
113
 
173
114
  pay_payment_method
174
115
  end
175
116
 
176
- def processor_subscription(subscription_id, options = {})
177
- ::Stripe::Subscription.retrieve(options.merge(id: subscription_id), stripe_options)
178
- end
117
+ ### Stripe extras
179
118
 
180
- def invoice!(options = {})
181
- return unless processor_id?
182
- ::Stripe::Invoice.create(options.merge(customer: processor_id), stripe_options).pay
119
+ # Creates and returns a Stripe::PaymentIntent
120
+ def create_payment_intent(amount, options = {})
121
+ args = {
122
+ amount: amount,
123
+ currency: "usd",
124
+ customer: processor_id || api_record.id,
125
+ expand: Pay::Stripe::Charge::EXPAND.map { |option| "latest_charge.#{option}" },
126
+ return_url: root_url
127
+ }.merge(options)
128
+
129
+ ::Stripe::PaymentIntent.create(args, stripe_options)
183
130
  end
184
131
 
185
- def upcoming_invoice
186
- ::Stripe::Invoice.upcoming({customer: processor_id}, stripe_options)
132
+ # Used for creating Stripe Terminal charges
133
+ def terminal_charge(amount, options = {})
134
+ create_payment_intent(amount, options.merge(payment_method_types: ["card_present"], capture_method: "manual"))
187
135
  end
188
136
 
189
137
  def create_setup_intent(options = {})
190
- customer unless processor_id?
191
- ::Stripe::SetupIntent.create({
192
- customer: processor_id,
193
- usage: :off_session
194
- }.merge(options), stripe_options)
138
+ ::Stripe::SetupIntent.create({customer: processor_id || api_record.id, usage: :off_session}.merge(options), stripe_options)
139
+ end
140
+
141
+ def invoice!(options = {})
142
+ ::Stripe::Invoice.create(options.merge(customer: processor_id || api_record.id), stripe_options).pay
195
143
  end
196
144
 
197
- def trial_end_date(stripe_sub)
198
- # Times in Stripe are returned in UTC
199
- stripe_sub.trial_end.present? ? Time.at(stripe_sub.trial_end) : nil
145
+ def preview_invoice(**options)
146
+ ::Stripe::Invoice.create_preview(options.merge(customer: processor_id || api_record.id), stripe_options)
200
147
  end
201
148
 
202
149
  # Syncs a customer's subscriptions from Stripe to the database.
203
150
  # Note that by default canceled subscriptions are NOT returned by Stripe. In order to include them, use `sync_subscriptions(status: "all")`.
204
151
  def sync_subscriptions(**options)
205
- subscriptions = ::Stripe::Subscription.list(options.merge(customer: customer), stripe_options)
152
+ subscriptions = ::Stripe::Subscription.list(options.with_defaults(customer: processor_id), stripe_options)
206
153
  subscriptions.map do |subscription|
207
154
  Pay::Stripe::Subscription.sync(subscription.id)
208
155
  end
@@ -221,7 +168,7 @@ module Pay
221
168
  # checkout(line_items: "price_12345", allow_promotion_codes: true)
222
169
  #
223
170
  def checkout(**options)
224
- customer unless processor_id?
171
+ api_record unless processor_id?
225
172
  args = {
226
173
  customer: processor_id,
227
174
  mode: "payment"
@@ -261,7 +208,7 @@ module Pay
261
208
  # checkout_charge(amount: 15_00, name: "T-shirt", quantity: 2)
262
209
  #
263
210
  def checkout_charge(amount:, name:, quantity: 1, **options)
264
- customer unless processor_id?
211
+ api_record unless processor_id?
265
212
  currency = options.delete(:currency) || "usd"
266
213
  checkout(
267
214
  line_items: {
@@ -277,7 +224,7 @@ module Pay
277
224
  end
278
225
 
279
226
  def billing_portal(**options)
280
- customer unless processor_id?
227
+ api_record unless processor_id?
281
228
  args = {
282
229
  customer: processor_id,
283
230
  return_url: options.delete(:return_url) || root_url
@@ -285,10 +232,28 @@ module Pay
285
232
  ::Stripe::BillingPortal::Session.create(args.merge(options), stripe_options)
286
233
  end
287
234
 
235
+ def customer_session(**options)
236
+ api_record unless processor_id?
237
+ args = {customer: processor_id}
238
+ ::Stripe::CustomerSession.create(args.merge(options), stripe_options)
239
+ end
240
+
288
241
  def authorize(amount, options = {})
289
242
  charge(amount, options.merge(capture_method: :manual))
290
243
  end
291
244
 
245
+ # Creates a meter event to bill for usage
246
+ #
247
+ # create_meter_event(:api_request, value: 1)
248
+ # create_meter_event(:api_request, token: 7)
249
+ def create_meter_event(event_name, payload: {}, **options)
250
+ api_record unless processor_id?
251
+ ::Stripe::Billing::MeterEvent.create({
252
+ event_name: event_name,
253
+ payload: {stripe_customer_id: processor_id}.merge(payload)
254
+ }.merge(options))
255
+ end
256
+
292
257
  private
293
258
 
294
259
  # Options for Stripe requests
@@ -299,9 +264,11 @@ module Pay
299
264
  # Includes the `session_id` param for Stripe Checkout with existing params (and makes sure the curly braces aren't escaped)
300
265
  def merge_session_id_param(url)
301
266
  uri = URI.parse(url)
302
- uri.query = URI.encode_www_form(URI.decode_www_form(uri.query.to_s).to_h.merge("session_id" => "{CHECKOUT_SESSION_ID}").to_a)
267
+ uri.query = URI.encode_www_form(URI.decode_www_form(uri.query.to_s).to_h.merge("stripe_checkout_session_id" => "{CHECKOUT_SESSION_ID}").to_a)
303
268
  uri.to_s.gsub("%7BCHECKOUT_SESSION_ID%7D", "{CHECKOUT_SESSION_ID}")
304
269
  end
305
270
  end
306
271
  end
307
272
  end
273
+
274
+ ActiveSupport.run_load_hooks :pay_stripe_customer, Pay::Stripe::Customer
@@ -1,26 +1,9 @@
1
1
  module Pay
2
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
-
3
+ class Merchant < Pay::Merchant
13
4
  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)
5
+ stripe_account = ::Stripe::Account.create(options)
6
+ update(processor_id: stripe_account.id)
24
7
  stripe_account
25
8
  rescue ::Stripe::StripeError => e
26
9
  raise Pay::Stripe::Error, e
@@ -64,3 +47,5 @@ module Pay
64
47
  end
65
48
  end
66
49
  end
50
+
51
+ ActiveSupport.run_load_hooks :pay_stripe_merchant, Pay::Stripe::Merchant