pay 9.0.1 → 10.0.1
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/app/models/pay/braintree/subscription.rb +3 -2
- data/app/models/pay/charge.rb +5 -20
- data/app/models/pay/fake_processor/charge.rb +4 -0
- data/app/models/pay/lemon_squeezy/charge.rb +10 -0
- data/app/models/pay/paddle_billing/subscription.rb +3 -0
- data/app/models/pay/paddle_classic/subscription.rb +4 -1
- data/app/models/pay/stripe/charge.rb +45 -55
- data/app/models/pay/stripe/customer.rb +3 -3
- data/app/models/pay/stripe/subscription.rb +33 -43
- data/app/models/pay/subscription.rb +0 -10
- data/config/locales/en.yml +1 -0
- data/db/migrate/20250415151129_add_object_to_pay_models.rb +7 -0
- data/lib/pay/receipts.rb +39 -38
- data/lib/pay/stripe/webhooks/customer_updated.rb +12 -8
- data/lib/pay/stripe.rb +1 -2
- data/lib/pay/version.rb +1 -1
- metadata +6 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 97c43263ab73cd849da8c65e8a4f1235aabb28b6f35c56c5441c077838374186
|
4
|
+
data.tar.gz: c0161223fdbd12eee31ef928639bba40e9af1a63e2e6cdeee3e2a69fce106de4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1fd5fdc16c976d6d33e908210c54de32cdbfcea6e0d641b58eb5f6e52fb5ff217bb59ac6ae3bb7118f8e59f5f6bcc2f2bd88fd61736d01c9fff386ebf24937ef
|
7
|
+
data.tar.gz: ffee6759b0bae293ffadb173158a57bac35e9f10150a38846499e2ffe635380a64606847098303a9422addd23e7e372d08669929b973c4135df2f2f81a3a0d2c
|
@@ -131,8 +131,9 @@ module Pay
|
|
131
131
|
end
|
132
132
|
|
133
133
|
braintree_plan = find_braintree_plan(plan)
|
134
|
+
prorate = options.fetch(:prorate) { true }
|
134
135
|
|
135
|
-
if would_change_billing_frequency?(braintree_plan) && prorate
|
136
|
+
if would_change_billing_frequency?(braintree_plan) && prorate
|
136
137
|
swap_across_frequencies(braintree_plan)
|
137
138
|
return
|
138
139
|
end
|
@@ -143,7 +144,7 @@ module Pay
|
|
143
144
|
never_expires: true,
|
144
145
|
number_of_billing_cycles: nil,
|
145
146
|
options: {
|
146
|
-
prorate_charges: prorate
|
147
|
+
prorate_charges: prorate
|
147
148
|
}
|
148
149
|
})
|
149
150
|
raise Error, "Braintree failed to swap plans: #{result.message}" unless result.success?
|
data/app/models/pay/charge.rb
CHANGED
@@ -23,20 +23,8 @@ module Pay
|
|
23
23
|
store_accessor :data, :username # Venmo
|
24
24
|
store_accessor :data, :bank
|
25
25
|
|
26
|
-
store_accessor :data, :
|
27
|
-
store_accessor :data, :
|
28
|
-
store_accessor :data, :payment_intent_id
|
29
|
-
store_accessor :data, :period_start
|
30
|
-
store_accessor :data, :period_end
|
31
|
-
store_accessor :data, :line_items
|
32
|
-
store_accessor :data, :subtotal # subtotal amount in cents
|
33
|
-
store_accessor :data, :tax # total tax amount in cents
|
34
|
-
store_accessor :data, :discounts # array of discount IDs applied to the Stripe Invoice
|
35
|
-
store_accessor :data, :total_discount_amounts # array of discount details
|
36
|
-
store_accessor :data, :total_tax_amounts # array of tax details for each jurisdiction
|
37
|
-
store_accessor :data, :credit_notes # array of credit notes for the Stripe Invoice
|
38
|
-
store_accessor :data, :refunds # array of refunds
|
39
|
-
store_accessor :data, :balance_transaction
|
26
|
+
store_accessor :data, :subtotal
|
27
|
+
store_accessor :data, :tax
|
40
28
|
|
41
29
|
# Helpers for payment processors
|
42
30
|
%w[braintree stripe paddle_billing paddle_classic lemon_squeezy fake_processor].each do |processor_name|
|
@@ -51,8 +39,9 @@ module Pay
|
|
51
39
|
joins(:customer).find_by(processor_id: processor_id, pay_customers: {processor: processor})
|
52
40
|
end
|
53
41
|
|
54
|
-
def
|
55
|
-
|
42
|
+
def sync!(**options)
|
43
|
+
self.class.sync(processor_id, **options)
|
44
|
+
reload
|
56
45
|
end
|
57
46
|
|
58
47
|
def refunded?
|
@@ -113,9 +102,5 @@ module Pay
|
|
113
102
|
payment_method_type&.titleize
|
114
103
|
end
|
115
104
|
end
|
116
|
-
|
117
|
-
def line_items
|
118
|
-
Array.wrap(super)
|
119
|
-
end
|
120
105
|
end
|
121
106
|
end
|
@@ -68,6 +68,16 @@ module Pay
|
|
68
68
|
end
|
69
69
|
end
|
70
70
|
|
71
|
+
def save
|
72
|
+
ls_type, ls_id = processor_id.split(":", 2)
|
73
|
+
case ls_type
|
74
|
+
when "order"
|
75
|
+
self.class.sync_order(ls_id)
|
76
|
+
when "subscription_invoice"
|
77
|
+
self.class.sync_subscription_invoice(ls_id)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
71
81
|
def api_record
|
72
82
|
ls_type, ls_id = processor_id.split(":", 2)
|
73
83
|
case ls_type
|
@@ -1,6 +1,9 @@
|
|
1
1
|
module Pay
|
2
2
|
module PaddleBilling
|
3
3
|
class Subscription < Pay::Subscription
|
4
|
+
store_accessor :data, :paddle_update_url
|
5
|
+
store_accessor :data, :paddle_cancel_url
|
6
|
+
|
4
7
|
def self.sync_from_transaction(transaction_id)
|
5
8
|
transaction = ::Paddle::Transaction.retrieve(id: transaction_id)
|
6
9
|
sync(transaction.subscription_id) if transaction.subscription_id
|
@@ -1,6 +1,9 @@
|
|
1
1
|
module Pay
|
2
2
|
module PaddleClassic
|
3
3
|
class Subscription < Pay::Subscription
|
4
|
+
store_accessor :data, :paddle_update_url
|
5
|
+
store_accessor :data, :paddle_cancel_url
|
6
|
+
|
4
7
|
def self.sync(subscription_id, object: nil, name: Pay.default_product_name)
|
5
8
|
# Passthrough is not return from this API, so we can't use that
|
6
9
|
object ||= PaddleClassic.client.users.list(subscription_id: subscription_id).data.try(:first)
|
@@ -128,7 +131,7 @@ module Pay
|
|
128
131
|
def swap(plan, **options)
|
129
132
|
raise ArgumentError, "plan must be a string" unless plan.is_a?(String)
|
130
133
|
|
131
|
-
attributes = {plan_id: plan, prorate: prorate}
|
134
|
+
attributes = {plan_id: plan, prorate: options.fetch(:prorate) { true }}
|
132
135
|
attributes[:quantity] = quantity if quantity?
|
133
136
|
PaddleClassic.client.users.update(subscription_id: processor_id, **attributes)
|
134
137
|
|
@@ -1,11 +1,21 @@
|
|
1
1
|
module Pay
|
2
2
|
module Stripe
|
3
3
|
class Charge < Pay::Charge
|
4
|
+
EXPAND = ["balance_transaction", "refunds"]
|
5
|
+
|
6
|
+
delegate :amount_captured, :payment_intent, to: :stripe_object
|
7
|
+
|
8
|
+
store_accessor :data, :stripe_invoice
|
4
9
|
store_accessor :data, :stripe_receipt_url
|
5
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
|
+
|
6
16
|
def self.sync(charge_id, object: nil, stripe_account: nil, try: 0, retries: 1)
|
7
17
|
# Skip loading the latest charge details from the API if we already have it
|
8
|
-
object ||= ::Stripe::Charge.retrieve({id: charge_id, expand:
|
18
|
+
object ||= ::Stripe::Charge.retrieve({id: charge_id, expand: EXPAND}, {stripe_account: stripe_account}.compact)
|
9
19
|
if object.customer.blank?
|
10
20
|
Rails.logger.debug "Stripe Charge #{object.id} does not have a customer"
|
11
21
|
return
|
@@ -17,68 +27,34 @@ module Pay
|
|
17
27
|
return
|
18
28
|
end
|
19
29
|
|
20
|
-
refunds = []
|
21
|
-
object.refunds.auto_paging_each { |refund| refunds << refund }
|
22
|
-
|
23
30
|
payment_method = object.payment_method_details.try(object.payment_method_details.type)
|
24
31
|
attrs = {
|
32
|
+
object: object.to_hash,
|
25
33
|
amount: object.amount,
|
26
|
-
amount_captured: object.amount_captured,
|
27
34
|
amount_refunded: object.amount_refunded,
|
28
35
|
application_fee_amount: object.application_fee_amount,
|
29
|
-
balance_transaction: object.balance_transaction,
|
30
36
|
bank: payment_method.try(:bank_name) || payment_method.try(:bank), # eps, fpx, ideal, p24, acss_debit, etc
|
31
37
|
brand: payment_method.try(:brand)&.capitalize,
|
32
38
|
created_at: Time.at(object.created),
|
33
39
|
currency: object.currency,
|
34
|
-
discounts: [],
|
35
40
|
exp_month: payment_method.try(:exp_month).to_s,
|
36
41
|
exp_year: payment_method.try(:exp_year).to_s,
|
37
42
|
last4: payment_method.try(:last4).to_s,
|
38
|
-
line_items: [],
|
39
43
|
metadata: object.metadata,
|
40
|
-
payment_intent_id: object.payment_intent,
|
41
44
|
payment_method_type: object.payment_method_details.type,
|
42
45
|
stripe_account: pay_customer.stripe_account,
|
43
|
-
stripe_receipt_url: object.receipt_url
|
44
|
-
total_tax_amounts: [],
|
45
|
-
refunds: refunds.sort_by! { |r| r["created"] }
|
46
|
+
stripe_receipt_url: object.receipt_url
|
46
47
|
}
|
47
48
|
|
48
49
|
# Associate charge with subscription if we can
|
49
|
-
|
50
|
-
|
51
|
-
attrs[:
|
52
|
-
attrs[:subscription] = pay_customer.subscriptions.find_by(processor_id: invoice.subscription)
|
53
|
-
|
54
|
-
attrs[:period_start] = Time.at(invoice.period_start)
|
55
|
-
attrs[:period_end] = Time.at(invoice.period_end)
|
50
|
+
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)
|
51
|
+
if invoice_payments.any? && (invoice = invoice_payments.first.invoice)
|
52
|
+
attrs[:stripe_invoice] = invoice.to_hash
|
56
53
|
attrs[:subtotal] = invoice.subtotal
|
57
|
-
attrs[:tax] = invoice.
|
58
|
-
|
59
|
-
|
60
|
-
attrs[:total_discount_amounts] = invoice.total_discount_amounts.map(&:to_hash)
|
61
|
-
|
62
|
-
invoice.lines.auto_paging_each do |line_item|
|
63
|
-
# Currency is tied to the charge, so storing it would be duplication
|
64
|
-
attrs[:line_items] << {
|
65
|
-
id: line_item.id,
|
66
|
-
description: line_item.description,
|
67
|
-
price_id: line_item.price&.id,
|
68
|
-
quantity: line_item.quantity,
|
69
|
-
unit_amount: line_item.price&.unit_amount,
|
70
|
-
amount: line_item.amount,
|
71
|
-
discounts: line_item.discounts,
|
72
|
-
tax_amounts: line_item.tax_amounts,
|
73
|
-
proration: line_item.proration,
|
74
|
-
period_start: Time.at(line_item.period.start),
|
75
|
-
period_end: Time.at(line_item.period.end)
|
76
|
-
}
|
54
|
+
attrs[:tax] = invoice.total - invoice.total_excluding_tax.to_i
|
55
|
+
if (subscription = invoice.parent.try(:subscription_details).try(:subscription))
|
56
|
+
attrs[:subscription] = pay_customer.subscriptions.find_by(processor_id: subscription)
|
77
57
|
end
|
78
|
-
# Charges without invoices
|
79
|
-
else
|
80
|
-
attrs[:period_start] = Time.at(object.created)
|
81
|
-
attrs[:period_end] = Time.at(object.created)
|
82
58
|
end
|
83
59
|
|
84
60
|
# Update or create the charge
|
@@ -90,16 +66,14 @@ module Pay
|
|
90
66
|
end
|
91
67
|
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
|
92
68
|
try += 1
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
raise
|
98
|
-
end
|
69
|
+
raise unless try <= retries
|
70
|
+
|
71
|
+
sleep 0.1
|
72
|
+
retry
|
99
73
|
end
|
100
74
|
|
101
75
|
def api_record
|
102
|
-
::Stripe::Charge.retrieve({id: processor_id, expand:
|
76
|
+
::Stripe::Charge.retrieve({id: processor_id, expand: EXPAND}, stripe_options)
|
103
77
|
rescue ::Stripe::StripeError => e
|
104
78
|
raise Pay::Stripe::Error, e
|
105
79
|
end
|
@@ -116,7 +90,7 @@ module Pay
|
|
116
90
|
def refund!(amount_to_refund, **options)
|
117
91
|
amount_to_refund ||= amount
|
118
92
|
|
119
|
-
if
|
93
|
+
if stripe_invoice.present?
|
120
94
|
description = options.delete(:description) || I18n.t("pay.refund")
|
121
95
|
lines = [{type: :custom_line_item, description: description, quantity: 1, unit_amount: amount_to_refund}]
|
122
96
|
credit_note!(**options.merge(refund_amount: amount_to_refund, lines: lines))
|
@@ -130,8 +104,9 @@ module Pay
|
|
130
104
|
|
131
105
|
# Adds a credit note to a Stripe Invoice
|
132
106
|
def credit_note!(**options)
|
133
|
-
raise Pay::Stripe::Error, "no Stripe
|
134
|
-
|
107
|
+
raise Pay::Stripe::Error, "no Stripe Invoice on Pay::Charge" if stripe_invoice.blank?
|
108
|
+
|
109
|
+
::Stripe::CreditNote.create({invoice: stripe_invoice.id}.merge(options), stripe_options)
|
135
110
|
rescue ::Stripe::StripeError => e
|
136
111
|
raise Pay::Stripe::Error, e
|
137
112
|
end
|
@@ -141,13 +116,28 @@ module Pay
|
|
141
116
|
# capture
|
142
117
|
# capture(amount_to_capture: 15_00)
|
143
118
|
def capture(**options)
|
144
|
-
raise Pay::Stripe::Error, "no
|
145
|
-
|
119
|
+
raise Pay::Stripe::Error, "no payment_intent on charge" unless payment_intent.present?
|
120
|
+
|
121
|
+
::Stripe::PaymentIntent.capture(payment_intent, options, stripe_options)
|
146
122
|
self.class.sync(processor_id)
|
147
123
|
rescue ::Stripe::StripeError => e
|
148
124
|
raise Pay::Stripe::Error, e
|
149
125
|
end
|
150
126
|
|
127
|
+
def captured?
|
128
|
+
amount_captured > 0
|
129
|
+
end
|
130
|
+
|
131
|
+
def stripe_invoice
|
132
|
+
if (value = data.dig("stripe_invoice"))
|
133
|
+
::Stripe::Invoice.construct_from(value)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def stripe_object
|
138
|
+
::Stripe::Charge.construct_from(object) if object?
|
139
|
+
end
|
140
|
+
|
151
141
|
private
|
152
142
|
|
153
143
|
# Options for Stripe requests
|
@@ -58,8 +58,7 @@ module Pay
|
|
58
58
|
def subscribe(name: Pay.default_product_name, plan: Pay.default_plan_name, **options)
|
59
59
|
quantity = options.delete(:quantity)
|
60
60
|
opts = {
|
61
|
-
|
62
|
-
items: [plan: plan, quantity: quantity]
|
61
|
+
items: [price: plan, quantity: quantity]
|
63
62
|
}.merge(options)
|
64
63
|
|
65
64
|
# Load the Stripe customer to verify it exists and update payment method if needed
|
@@ -73,7 +72,8 @@ module Pay
|
|
73
72
|
|
74
73
|
# No trial, payment method requires SCA
|
75
74
|
if options[:payment_behavior].to_s != "default_incomplete" && subscription.incomplete?
|
76
|
-
|
75
|
+
payment_intent_id = stripe_sub.latest_invoice.payments.first.payment.payment_intent
|
76
|
+
Pay::Payment.from_id(payment_intent_id).validate
|
77
77
|
end
|
78
78
|
|
79
79
|
subscription
|
@@ -23,6 +23,7 @@ module Pay
|
|
23
23
|
end
|
24
24
|
|
25
25
|
attributes = {
|
26
|
+
object: object.to_hash,
|
26
27
|
application_fee_percent: object.application_fee_percent,
|
27
28
|
created_at: Time.at(object.created),
|
28
29
|
processor_plan: object.items.first.price.id,
|
@@ -30,12 +31,11 @@ module Pay
|
|
30
31
|
status: object.status,
|
31
32
|
stripe_account: pay_customer.stripe_account,
|
32
33
|
metadata: object.metadata,
|
33
|
-
subscription_items: [],
|
34
34
|
metered: false,
|
35
35
|
pause_behavior: object.pause_collection&.behavior,
|
36
36
|
pause_resumes_at: (object.pause_collection&.resumes_at ? Time.at(object.pause_collection&.resumes_at) : nil),
|
37
|
-
current_period_start: (object.current_period_start ? Time.at(object.current_period_start) : nil),
|
38
|
-
current_period_end: (object.current_period_end ? Time.at(object.current_period_end) : nil)
|
37
|
+
current_period_start: (object.items.first.current_period_start ? Time.at(object.items.first.current_period_start) : nil),
|
38
|
+
current_period_end: (object.items.first.current_period_end ? Time.at(object.items.first.current_period_end) : nil)
|
39
39
|
}
|
40
40
|
|
41
41
|
# Subscriptions that have ended should have their trial ended at the
|
@@ -49,13 +49,9 @@ module Pay
|
|
49
49
|
attributes[:trial_ends_at] = Time.at(trial_ended_at)
|
50
50
|
end
|
51
51
|
|
52
|
-
# Record subscription items to db
|
53
52
|
object.items.auto_paging_each do |subscription_item|
|
54
|
-
if
|
55
|
-
|
56
|
-
end
|
57
|
-
|
58
|
-
attributes[:subscription_items] << subscription_item.to_hash.slice(:id, :price, :metadata, :quantity)
|
53
|
+
next if attributes[:metered]
|
54
|
+
attributes[:metered] = true if subscription_item.price.try(:recurring).try(:usage_type) == "metered"
|
59
55
|
end
|
60
56
|
|
61
57
|
attributes[:ends_at] = if object.ended_at
|
@@ -66,7 +62,7 @@ module Pay
|
|
66
62
|
Time.at(object.cancel_at)
|
67
63
|
elsif object.cancel_at_period_end
|
68
64
|
# Subscriptions cancelling in the future
|
69
|
-
Time.at(object.current_period_end)
|
65
|
+
Time.at(object.items.first.current_period_end)
|
70
66
|
end
|
71
67
|
|
72
68
|
# Sync payment method if directly attached to subscription
|
@@ -87,7 +83,7 @@ module Pay
|
|
87
83
|
# Any other pause status (or no pause at all) should have nil for start
|
88
84
|
if pay_subscription.pause_behavior != attributes[:pause_behavior]
|
89
85
|
attributes[:pause_starts_at] = if attributes[:pause_behavior] == "void"
|
90
|
-
Time.at(object.current_period_end)
|
86
|
+
Time.at(object.items.first.current_period_end)
|
91
87
|
end
|
92
88
|
end
|
93
89
|
|
@@ -102,8 +98,11 @@ module Pay
|
|
102
98
|
pay_subscription.api_record = object
|
103
99
|
|
104
100
|
# Sync the latest charge if we already have it loaded (like during subscrbe), otherwise, let webhooks take care of creating it
|
105
|
-
if (
|
106
|
-
|
101
|
+
if (invoice = object.try(:latest_invoice))
|
102
|
+
Array(invoice.try(:payments)).each do |invoice_payment|
|
103
|
+
next unless invoice_payment.status == "paid"
|
104
|
+
Pay::Stripe::Charge.sync_payment_intent(invoice_payment.payment.payment_intent, stripe_account: pay_subscription.stripe_account)
|
105
|
+
end
|
107
106
|
end
|
108
107
|
|
109
108
|
pay_subscription
|
@@ -121,23 +120,27 @@ module Pay
|
|
121
120
|
def self.expand_options
|
122
121
|
{
|
123
122
|
expand: [
|
123
|
+
"discounts",
|
124
124
|
"default_payment_method",
|
125
125
|
"pending_setup_intent",
|
126
|
-
"latest_invoice.
|
127
|
-
"latest_invoice.
|
128
|
-
"latest_invoice.total_discount_amounts.discount"
|
129
|
-
"latest_invoice.total_tax_amounts.tax_rate"
|
126
|
+
"latest_invoice.confirmation_secret",
|
127
|
+
"latest_invoice.payments",
|
128
|
+
"latest_invoice.total_discount_amounts.discount"
|
130
129
|
]
|
131
130
|
}
|
132
131
|
end
|
133
132
|
|
133
|
+
def stripe_object
|
134
|
+
::Stripe::Subscription.construct_from(object)
|
135
|
+
end
|
136
|
+
|
134
137
|
def api_record(**options)
|
135
138
|
@api_record ||= ::Stripe::Subscription.retrieve(options.with_defaults(id: processor_id).merge(expand_options), {stripe_account: stripe_account}.compact)
|
136
139
|
end
|
137
140
|
|
138
141
|
# Returns a SetupIntent or PaymentIntent client secret for the subscription
|
139
142
|
def client_secret
|
140
|
-
api_record&.pending_setup_intent&.client_secret || api_record&.latest_invoice&.
|
143
|
+
api_record&.pending_setup_intent&.client_secret || api_record&.latest_invoice&.confirmation_secret&.client_secret
|
141
144
|
end
|
142
145
|
|
143
146
|
# Sets the default_payment_method on a subscription
|
@@ -160,7 +163,7 @@ module Pay
|
|
160
163
|
cancel_now!
|
161
164
|
else
|
162
165
|
@api_record = ::Stripe::Subscription.update(processor_id, {cancel_at_period_end: true}.merge(expand_options), stripe_options)
|
163
|
-
update(ends_at: (on_trial? ? trial_ends_at : Time.at(@api_record.current_period_end)))
|
166
|
+
update(ends_at: (on_trial? ? trial_ends_at : Time.at(@api_record.items.first.current_period_end)))
|
164
167
|
end
|
165
168
|
rescue ::Stripe::StripeError => e
|
166
169
|
raise Pay::Stripe::Error, e
|
@@ -184,7 +187,7 @@ module Pay
|
|
184
187
|
# For a subscription with a single item, we can update the subscription directly if no SubscriptionItem ID is available
|
185
188
|
# Otherwise a SubscriptionItem ID is required so Stripe knows which entry to update
|
186
189
|
def change_quantity(quantity, **options)
|
187
|
-
subscription_item_id = options.delete(:subscription_item_id) || subscription_items&.first&.
|
190
|
+
subscription_item_id = options.delete(:subscription_item_id) || subscription_items&.first&.id
|
188
191
|
if subscription_item_id
|
189
192
|
::Stripe::SubscriptionItem.update(subscription_item_id, options.merge(quantity: quantity), stripe_options)
|
190
193
|
@api_record = nil
|
@@ -234,7 +237,7 @@ module Pay
|
|
234
237
|
update(
|
235
238
|
pause_behavior: behavior,
|
236
239
|
pause_resumes_at: (@api_record.pause_collection&.resumes_at ? Time.at(@api_record.pause_collection&.resumes_at) : nil),
|
237
|
-
pause_starts_at: ((behavior == "void") ? Time.at(@api_record.current_period_end) : nil)
|
240
|
+
pause_starts_at: ((behavior == "void") ? Time.at(@api_record.items.first.current_period_end) : nil)
|
238
241
|
)
|
239
242
|
end
|
240
243
|
|
@@ -277,6 +280,7 @@ module Pay
|
|
277
280
|
def swap(plan, **options)
|
278
281
|
raise ArgumentError, "plan must be a string" unless plan.is_a?(String)
|
279
282
|
|
283
|
+
prorate = options.fetch(:prorate) { true }
|
280
284
|
proration_behavior = options.delete(:proration_behavior) || (prorate ? "always_invoice" : "none")
|
281
285
|
|
282
286
|
@api_record = ::Stripe::Subscription.update(
|
@@ -292,8 +296,8 @@ module Pay
|
|
292
296
|
)
|
293
297
|
|
294
298
|
# Validate that swap was successful and handle SCA if needed
|
295
|
-
if (
|
296
|
-
Pay::Payment.
|
299
|
+
if (payment_intent_id = @api_record.latest_invoice.payments.first.payment.payment_intent)
|
300
|
+
Pay::Payment.from_id(payment_intent_id).validate
|
297
301
|
end
|
298
302
|
|
299
303
|
sync!(object: @api_record)
|
@@ -301,33 +305,19 @@ module Pay
|
|
301
305
|
raise Pay::Stripe::Error, e
|
302
306
|
end
|
303
307
|
|
304
|
-
|
305
|
-
|
306
|
-
# Uses the first subscription_item ID unless `subscription_item_id: "si_1234"` is passed
|
307
|
-
#
|
308
|
-
# create_usage_record(quantity: 4, action: :increment)
|
309
|
-
# create_usage_record(subscription_item_id: "si_1234", quantity: 100, action: :set)
|
310
|
-
def create_usage_record(**options)
|
311
|
-
subscription_item_id = options.delete(:subscription_item_id) || metered_subscription_item&.dig("id")
|
312
|
-
::Stripe::SubscriptionItem.create_usage_record(subscription_item_id, options, stripe_options)
|
313
|
-
end
|
314
|
-
|
315
|
-
# Returns usage record summaries for a subscription item
|
316
|
-
def usage_record_summaries(**options)
|
317
|
-
subscription_item_id = options.delete(:subscription_item_id) || metered_subscription_item&.dig("id")
|
318
|
-
::Stripe::SubscriptionItem.list_usage_record_summaries(subscription_item_id, options, stripe_options)
|
308
|
+
def subscription_items
|
309
|
+
stripe_object.items
|
319
310
|
end
|
320
311
|
|
321
312
|
# Returns the first metered subscription item
|
322
313
|
def metered_subscription_item
|
323
|
-
subscription_items.
|
324
|
-
subscription_item.
|
314
|
+
subscription_items.auto_paging_each do |subscription_item|
|
315
|
+
return subscription_item if subscription_item.price.try(:recurring).try(:usage_type) == "metered"
|
325
316
|
end
|
326
317
|
end
|
327
318
|
|
328
|
-
|
329
|
-
|
330
|
-
::Stripe::Invoice.upcoming(options.merge(subscription: processor_id), stripe_options)
|
319
|
+
def preview_invoice(**options)
|
320
|
+
::Stripe::Invoice.create_preview(options.merge(subscription: processor_id), stripe_options)
|
331
321
|
end
|
332
322
|
|
333
323
|
# Retries the latest invoice for a Past Due subscription and attempts to pay it
|
@@ -27,12 +27,6 @@ module Pay
|
|
27
27
|
# Callbacks
|
28
28
|
before_destroy :cancel_if_active
|
29
29
|
|
30
|
-
store_accessor :data, :paddle_update_url
|
31
|
-
store_accessor :data, :paddle_cancel_url
|
32
|
-
store_accessor :data, :subscription_items
|
33
|
-
|
34
|
-
attribute :prorate, :boolean, default: true
|
35
|
-
|
36
30
|
# Validations
|
37
31
|
validates :name, presence: true
|
38
32
|
validates :processor_id, presence: true, uniqueness: {scope: :customer_id, case_sensitive: true}
|
@@ -58,10 +52,6 @@ module Pay
|
|
58
52
|
reload
|
59
53
|
end
|
60
54
|
|
61
|
-
def no_prorate
|
62
|
-
self.prorate = false
|
63
|
-
end
|
64
|
-
|
65
55
|
def skip_trial
|
66
56
|
self.trial_ends_at = nil
|
67
57
|
end
|
data/config/locales/en.yml
CHANGED
@@ -0,0 +1,7 @@
|
|
1
|
+
class AddObjectToPayModels < ActiveRecord::Migration[6.0]
|
2
|
+
def change
|
3
|
+
add_column :pay_charges, :object, Pay::Adapter.json_column_type
|
4
|
+
add_column :pay_customers, :object, Pay::Adapter.json_column_type
|
5
|
+
add_column :pay_subscriptions, :object, Pay::Adapter.json_column_type
|
6
|
+
end
|
7
|
+
end
|
data/lib/pay/receipts.rb
CHANGED
@@ -31,17 +31,13 @@ module Pay
|
|
31
31
|
]
|
32
32
|
]
|
33
33
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
Array.wrap(li["discounts"]).each do |discount_id|
|
42
|
-
if (discount = total_discount_amounts.find { |d| d.dig("discount", "id") == discount_id })
|
43
|
-
items << [discount_description(discount), nil, nil, Pay::Currency.format(-discount["amount"], currency: currency)]
|
44
|
-
end
|
34
|
+
if stripe_invoice
|
35
|
+
stripe_invoice.lines.auto_paging_each do |line|
|
36
|
+
items << [line.description, line.quantity, Pay::Currency.format(line.pricing.unit_amount_decimal, currency: line.currency), Pay::Currency.format(line.amount, currency: line.currency)]
|
37
|
+
|
38
|
+
line.discounts.each do |discount_id|
|
39
|
+
discount = stripe_invoice.total_discount_amounts.find { |d| d.discount.id == discount_id }
|
40
|
+
items << [discount_description(discount), nil, nil, Pay::Currency.format(-discount.amount, currency: currency)]
|
45
41
|
end
|
46
42
|
end
|
47
43
|
else
|
@@ -49,42 +45,47 @@ module Pay
|
|
49
45
|
end
|
50
46
|
|
51
47
|
# If no subtotal, we will display the total
|
52
|
-
items << [nil, nil, I18n.t("pay.line_items.subtotal"), Pay::Currency.format(subtotal || amount, currency: currency)]
|
48
|
+
items << [nil, nil, I18n.t("pay.line_items.subtotal"), Pay::Currency.format(stripe_invoice&.subtotal || amount, currency: currency)]
|
53
49
|
|
54
50
|
# Discounts on the invoice
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
51
|
+
stripe_invoice&.discounts&.each do |discount_id|
|
52
|
+
discount = stripe_invoice.total_discount_amounts.find { |d| d.discount.id == discount_id }
|
53
|
+
items << [nil, nil, discount_description(discount), Pay::Currency.format(-discount.amount, currency: currency)]
|
54
|
+
end
|
55
|
+
|
56
|
+
# Total excluding tax
|
57
|
+
if stripe_invoice
|
58
|
+
items << [nil, nil, I18n.t("pay.line_items.total"), Pay::Currency.format(stripe_invoice.total_excluding_tax, currency: currency)]
|
59
59
|
end
|
60
60
|
|
61
61
|
# Tax rates
|
62
|
-
|
63
|
-
next if
|
64
|
-
|
62
|
+
stripe_invoice&.total_taxes&.each do |tax|
|
63
|
+
next if tax.amount.zero?
|
64
|
+
# tax_rate = ::Stripe::TaxRate.retrieve(tax.tax_rate_details.tax_rate)
|
65
|
+
items << [nil, nil, I18n.t("pay.line_items.tax"), Pay::Currency.format(tax.amount, currency: currency)]
|
65
66
|
end
|
66
67
|
|
68
|
+
# Total
|
67
69
|
items << [nil, nil, I18n.t("pay.line_items.total"), Pay::Currency.format(amount, currency: currency)]
|
68
70
|
items
|
69
71
|
end
|
70
72
|
|
71
73
|
def discount_description(discount)
|
72
|
-
coupon = discount.
|
73
|
-
name = coupon.
|
74
|
+
coupon = discount.discount.coupon
|
75
|
+
name = coupon.name
|
74
76
|
|
75
|
-
if (percent = coupon
|
77
|
+
if (percent = coupon.percent_off)
|
76
78
|
I18n.t("pay.line_items.percent_discount", name: name, percent: ActiveSupport::NumberHelper.number_to_rounded(percent, strip_insignificant_zeros: true))
|
77
79
|
else
|
78
|
-
I18n.t("pay.line_items.amount_discount", name: name, amount: Pay::Currency.format(coupon
|
80
|
+
I18n.t("pay.line_items.amount_discount", name: name, amount: Pay::Currency.format(coupon.amount_off, currency: coupon.currency))
|
79
81
|
end
|
80
82
|
end
|
81
83
|
|
82
|
-
def tax_description(
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
end
|
84
|
+
# def tax_description(tax_rate)
|
85
|
+
# percent = "#{ActiveSupport::NumberHelper.number_to_rounded(tax_rate.percentage, strip_insignificant_zeros: true)}%"
|
86
|
+
# percent += " inclusive" if tax_rate.inclusive
|
87
|
+
# "#{tax_rate.display_name} - #{tax_rate.jurisdiction} (#{percent})"
|
88
|
+
# end
|
88
89
|
|
89
90
|
def receipt_line_items
|
90
91
|
line_items = pdf_line_items
|
@@ -94,15 +95,15 @@ module Pay
|
|
94
95
|
|
95
96
|
if refunded?
|
96
97
|
# If we have a list of individual refunds, add each entry
|
97
|
-
if refunds&.any?
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
else
|
104
|
-
|
105
|
-
end
|
98
|
+
# if refunds&.any?
|
99
|
+
# refunds.each do |refund|
|
100
|
+
# next unless refund["status"] == "succeeded"
|
101
|
+
# refunded_at = Time.at(refund["created"]).to_date
|
102
|
+
# line_items << [nil, nil, I18n.t("pay.receipt.refunded_on", date: I18n.l(refunded_at, format: :long)), Pay::Currency.format(refund["amount"], currency: refund["currency"])]
|
103
|
+
# end
|
104
|
+
# else
|
105
|
+
line_items << [nil, nil, I18n.t("pay.receipt.refunded"), Pay::Currency.format(amount_refunded, currency: currency)]
|
106
|
+
# end
|
106
107
|
end
|
107
108
|
|
108
109
|
line_items
|
@@ -11,6 +11,18 @@ module Pay
|
|
11
11
|
|
12
12
|
stripe_customer = pay_customer.api_record
|
13
13
|
|
14
|
+
attributes = {
|
15
|
+
object: stripe_customer.to_hash
|
16
|
+
}
|
17
|
+
|
18
|
+
# Sync invoice credit balance and currency
|
19
|
+
if stripe_customer.invoice_credit_balance.present?
|
20
|
+
attributes[:invoice_credit_balance] = stripe_customer.invoice_credit_balance
|
21
|
+
attributes[:currency] = stripe_customer.currency
|
22
|
+
end
|
23
|
+
|
24
|
+
pay_customer.update(attributes)
|
25
|
+
|
14
26
|
# Sync default card
|
15
27
|
if (payment_method_id = stripe_customer.invoice_settings.default_payment_method)
|
16
28
|
Pay::Stripe::PaymentMethod.sync(payment_method_id, stripe_account: event.try(:account))
|
@@ -19,14 +31,6 @@ module Pay
|
|
19
31
|
# No default payment method set
|
20
32
|
pay_customer.payment_methods.update_all(default: false)
|
21
33
|
end
|
22
|
-
|
23
|
-
# Sync invoice credit balance and currency
|
24
|
-
if stripe_customer.invoice_credit_balance.present?
|
25
|
-
pay_customer.update(
|
26
|
-
invoice_credit_balance: stripe_customer.invoice_credit_balance,
|
27
|
-
currency: stripe_customer.currency
|
28
|
-
)
|
29
|
-
end
|
30
34
|
end
|
31
35
|
end
|
32
36
|
end
|
data/lib/pay/stripe.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
module Pay
|
2
2
|
module Stripe
|
3
3
|
class Error < Pay::Error
|
4
|
-
delegate :message, to: :cause
|
5
4
|
end
|
6
5
|
|
7
6
|
module Webhooks
|
@@ -27,7 +26,7 @@ module Pay
|
|
27
26
|
|
28
27
|
extend Env
|
29
28
|
|
30
|
-
REQUIRED_VERSION = "~>
|
29
|
+
REQUIRED_VERSION = "~> 15"
|
31
30
|
|
32
31
|
# A list of database model names that include Pay
|
33
32
|
# Used for safely looking up models with client_reference_id
|
data/lib/pay/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: pay
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 10.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jason Charnes
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
- Collin Jilbert
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|
@@ -17,14 +17,14 @@ dependencies:
|
|
17
17
|
requirements:
|
18
18
|
- - ">="
|
19
19
|
- !ruby/object:Gem::Version
|
20
|
-
version:
|
20
|
+
version: 7.0.0
|
21
21
|
type: :runtime
|
22
22
|
prerelease: false
|
23
23
|
version_requirements: !ruby/object:Gem::Requirement
|
24
24
|
requirements:
|
25
25
|
- - ">="
|
26
26
|
- !ruby/object:Gem::Version
|
27
|
-
version:
|
27
|
+
version: 7.0.0
|
28
28
|
description: Stripe, Paddle, and Braintree payments for Ruby on Rails apps
|
29
29
|
email:
|
30
30
|
- jason@thecharnes.com
|
@@ -99,6 +99,7 @@ files:
|
|
99
99
|
- config/locales/en.yml
|
100
100
|
- config/routes.rb
|
101
101
|
- db/migrate/1_create_pay_tables.rb
|
102
|
+
- db/migrate/20250415151129_add_object_to_pay_models.rb
|
102
103
|
- db/migrate/2_add_pay_sti_columns.rb
|
103
104
|
- lib/generators/pay/email_views_generator.rb
|
104
105
|
- lib/generators/pay/views_generator.rb
|
@@ -178,7 +179,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
178
179
|
- !ruby/object:Gem::Version
|
179
180
|
version: '0'
|
180
181
|
requirements: []
|
181
|
-
rubygems_version: 3.6.
|
182
|
+
rubygems_version: 3.6.8
|
182
183
|
specification_version: 4
|
183
184
|
summary: Payments engine for Ruby on Rails
|
184
185
|
test_files: []
|