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.
- checksums.yaml +4 -4
- data/README.md +8 -4
- data/app/controllers/pay/payments_controller.rb +2 -0
- data/app/controllers/pay/webhooks/lemon_squeezy_controller.rb +45 -0
- data/app/jobs/pay/customer_sync_job.rb +1 -1
- data/app/models/concerns/pay/routing.rb +13 -0
- data/{lib → app/models}/pay/braintree/charge.rb +7 -12
- data/{lib/pay/braintree/billable.rb → app/models/pay/braintree/customer.rb} +33 -71
- data/{lib → app/models}/pay/braintree/payment_method.rb +4 -10
- data/{lib → app/models}/pay/braintree/subscription.rb +23 -61
- data/app/models/pay/charge.rb +16 -45
- data/app/models/pay/customer.rb +5 -16
- data/app/models/pay/fake_processor/charge.rb +19 -0
- data/{lib/pay/fake_processor/billable.rb → app/models/pay/fake_processor/customer.rb} +28 -38
- data/{lib → app/models}/pay/fake_processor/merchant.rb +4 -9
- data/app/models/pay/fake_processor/payment_method.rb +13 -0
- data/app/models/pay/fake_processor/subscription.rb +70 -0
- data/app/models/pay/lemon_squeezy/charge.rb +96 -0
- data/app/models/pay/lemon_squeezy/customer.rb +80 -0
- data/app/models/pay/lemon_squeezy/payment_method.rb +29 -0
- data/app/models/pay/lemon_squeezy/subscription.rb +129 -0
- data/app/models/pay/merchant.rb +2 -11
- data/{lib → app/models}/pay/paddle_billing/charge.rb +15 -13
- data/{lib/pay/paddle_billing/billable.rb → app/models/pay/paddle_billing/customer.rb} +20 -35
- data/{lib → app/models}/pay/paddle_billing/payment_method.rb +13 -13
- data/{lib → app/models}/pay/paddle_billing/subscription.rb +40 -43
- data/{lib → app/models}/pay/paddle_classic/charge.rb +15 -18
- data/{lib/pay/paddle_classic/billable.rb → app/models/pay/paddle_classic/customer.rb} +11 -31
- data/{lib → app/models}/pay/paddle_classic/payment_method.rb +3 -11
- data/{lib → app/models}/pay/paddle_classic/subscription.rb +17 -37
- data/app/models/pay/payment_method.rb +4 -5
- data/app/models/pay/stripe/charge.rb +155 -0
- data/{lib/pay/stripe/billable.rb → app/models/pay/stripe/customer.rb} +78 -111
- data/{lib → app/models}/pay/stripe/merchant.rb +5 -20
- data/{lib → app/models}/pay/stripe/payment_method.rb +11 -17
- data/{lib → app/models}/pay/stripe/subscription.rb +83 -112
- data/app/models/pay/subscription.rb +13 -47
- data/app/models/pay/webhook.rb +5 -1
- data/app/views/pay/user_mailer/payment_action_required.text.erb +9 -0
- data/app/views/pay/user_mailer/payment_failed.text.erb +9 -0
- data/app/views/pay/user_mailer/receipt.text.erb +20 -0
- data/app/views/pay/user_mailer/refund.text.erb +21 -0
- data/app/views/pay/user_mailer/subscription_renewing.text.erb +8 -0
- data/app/views/pay/user_mailer/subscription_trial_ended.text.erb +8 -0
- data/app/views/pay/user_mailer/subscription_trial_will_end.text.erb +8 -0
- data/config/locales/en.yml +1 -0
- data/config/routes.rb +1 -0
- data/db/migrate/20250415151129_add_object_to_pay_models.rb +7 -0
- data/db/migrate/2_add_pay_sti_columns.rb +24 -0
- data/lib/pay/attributes.rb +16 -8
- data/lib/pay/braintree.rb +25 -6
- data/lib/pay/engine.rb +2 -0
- data/lib/pay/fake_processor.rb +2 -6
- data/lib/pay/lemon_squeezy/webhooks/order.rb +11 -0
- data/lib/pay/lemon_squeezy/webhooks/subscription.rb +3 -3
- data/lib/pay/lemon_squeezy/webhooks/subscription_payment.rb +11 -0
- data/lib/pay/lemon_squeezy.rb +58 -104
- data/lib/pay/nano_id.rb +1 -1
- data/lib/pay/paddle_billing.rb +15 -6
- data/lib/pay/paddle_classic/webhooks/signature_verifier.rb +1 -1
- data/lib/pay/paddle_classic.rb +11 -9
- data/lib/pay/receipts.rb +45 -44
- data/lib/pay/stripe/webhooks/charge_updated.rb +11 -0
- data/lib/pay/stripe/webhooks/customer_updated.rb +13 -9
- data/lib/pay/stripe/webhooks/payment_action_required.rb +10 -6
- data/lib/pay/stripe/webhooks/payment_failed.rb +6 -4
- data/lib/pay/stripe/webhooks/subscription_renewing.rb +9 -4
- data/lib/pay/stripe.rb +28 -9
- data/lib/pay/version.rb +1 -1
- data/lib/pay.rb +19 -1
- data/lib/tasks/pay.rake +2 -2
- metadata +45 -43
- data/app/views/pay/stripe/_checkout_button.html.erb +0 -21
- data/lib/pay/braintree/authorization_error.rb +0 -9
- data/lib/pay/braintree/error.rb +0 -23
- data/lib/pay/fake_processor/charge.rb +0 -21
- data/lib/pay/fake_processor/error.rb +0 -6
- data/lib/pay/fake_processor/payment_method.rb +0 -21
- data/lib/pay/fake_processor/subscription.rb +0 -90
- data/lib/pay/lemon_squeezy/billable.rb +0 -90
- data/lib/pay/lemon_squeezy/charge.rb +0 -68
- data/lib/pay/lemon_squeezy/error.rb +0 -7
- data/lib/pay/lemon_squeezy/payment_method.rb +0 -40
- data/lib/pay/lemon_squeezy/subscription.rb +0 -185
- data/lib/pay/lemon_squeezy/webhooks/transaction_completed.rb +0 -11
- data/lib/pay/paddle_billing/error.rb +0 -7
- data/lib/pay/paddle_classic/error.rb +0 -7
- data/lib/pay/stripe/charge.rb +0 -176
- data/lib/pay/stripe/error.rb +0 -7
@@ -1,26 +1,8 @@
|
|
1
1
|
module Pay
|
2
2
|
module PaddleClassic
|
3
|
-
class Subscription
|
4
|
-
|
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
|
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(
|
68
|
+
Time.parse(api_record.next_payment.date)
|
91
69
|
end
|
92
70
|
|
93
71
|
PaddleClassic.client.users.cancel(subscription_id: processor_id)
|
94
|
-
|
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:
|
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
|
-
|
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:
|
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
|
-
|
106
|
+
status == "paused"
|
129
107
|
end
|
130
108
|
|
131
109
|
def pause
|
132
110
|
response = PaddleClassic.client.users.pause(subscription_id: processor_id)
|
133
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
4
|
-
include
|
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
|
-
|
22
|
-
|
23
|
-
|
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
|
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,
|
15
|
+
owner.send(owner.class.pay_stripe_customer_attributes, self)
|
32
16
|
when Proc
|
33
|
-
owner.class.pay_stripe_customer_attributes.call(
|
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
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
70
|
-
|
71
|
-
|
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
|
-
|
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
|
-
|
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] =
|
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
|
-
|
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
|
-
|
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 =
|
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
|
-
|
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
|
-
|
112
|
+
reload_default_payment_method
|
172
113
|
|
173
114
|
pay_payment_method
|
174
115
|
end
|
175
116
|
|
176
|
-
|
177
|
-
::Stripe::Subscription.retrieve(options.merge(id: subscription_id), stripe_options)
|
178
|
-
end
|
117
|
+
### Stripe extras
|
179
118
|
|
180
|
-
|
181
|
-
|
182
|
-
|
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
|
-
|
186
|
-
|
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
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
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
|
198
|
-
|
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.
|
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
|
-
|
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
|
-
|
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
|
-
|
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("
|
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
|
-
|
15
|
-
|
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
|