pay 5.0.4 → 6.0.1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of pay might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/app/mailers/pay/user_mailer.rb +14 -6
- data/app/models/pay/customer.rb +5 -0
- data/app/models/pay/subscription.rb +37 -60
- data/app/views/pay/user_mailer/subscription_trial_ended.html.erb +6 -0
- data/app/views/pay/user_mailer/subscription_trial_will_end.html.erb +6 -0
- data/config/locales/en.yml +4 -0
- data/db/migrate/1_create_pay_tables.rb +8 -0
- data/lib/pay/billable/sync_customer.rb +1 -1
- data/lib/pay/braintree/subscription.rb +22 -2
- data/lib/pay/fake_processor/merchant.rb +26 -0
- data/lib/pay/fake_processor/subscription.rb +13 -4
- data/lib/pay/fake_processor.rb +1 -0
- data/lib/pay/paddle/subscription.rb +17 -6
- data/lib/pay/paddle/webhooks/subscription_updated.rb +1 -1
- data/lib/pay/stripe/billable.rb +4 -7
- data/lib/pay/stripe/charge.rb +2 -2
- data/lib/pay/stripe/payment_method.rb +25 -0
- data/lib/pay/stripe/subscription.rb +55 -17
- data/lib/pay/stripe/webhooks/payment_intent_succeeded.rb +1 -3
- data/lib/pay/stripe/webhooks/payment_method_attached.rb +1 -4
- data/lib/pay/stripe/webhooks/payment_method_updated.rb +0 -6
- data/lib/pay/stripe/webhooks/subscription_trial_will_end.rb +24 -0
- data/lib/pay/stripe.rb +6 -2
- data/lib/pay/version.rb +1 -1
- data/lib/pay.rb +23 -0
- data/lib/tasks/pay.rake +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 60ae2539f022f791877543b87528991b3a549a582e0b112110fd502b1bbb8482
|
4
|
+
data.tar.gz: ce373070abf9fda58b15eeb8f19ce51f915f168d937020668e2cf2640502a698
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 69f90f4002d0d47a51df89bb71de0ccfb07fa22b71ebe6b3246e23be47fd1ae8acb1f29358ff528d4420e72ab19cce585fa41693aa8cd61657225e1caa466224
|
7
|
+
data.tar.gz: ac708cb4e3a63b3640099a175b99e0072af6df4098890e5cce660365ac2bff9869e76d37d1a75284ea085eff07a60d6651c83b67264d507605566302dad9933b
|
@@ -5,25 +5,33 @@ module Pay
|
|
5
5
|
attachments[params[:pay_charge].filename] = params[:pay_charge].receipt
|
6
6
|
end
|
7
7
|
|
8
|
-
mail
|
8
|
+
mail mail_arguments
|
9
9
|
end
|
10
10
|
|
11
11
|
def refund
|
12
|
-
mail
|
12
|
+
mail mail_arguments
|
13
13
|
end
|
14
14
|
|
15
15
|
def subscription_renewing
|
16
|
-
mail
|
16
|
+
mail mail_arguments
|
17
17
|
end
|
18
18
|
|
19
19
|
def payment_action_required
|
20
|
-
mail
|
20
|
+
mail mail_arguments
|
21
|
+
end
|
22
|
+
|
23
|
+
def subscription_trial_will_end
|
24
|
+
mail mail_arguments
|
25
|
+
end
|
26
|
+
|
27
|
+
def subscription_trial_ended
|
28
|
+
mail mail_arguments
|
21
29
|
end
|
22
30
|
|
23
31
|
private
|
24
32
|
|
25
|
-
def
|
26
|
-
|
33
|
+
def mail_arguments
|
34
|
+
Pay.mail_arguments.call(mailer_name, params)
|
27
35
|
end
|
28
36
|
end
|
29
37
|
end
|
data/app/models/pay/customer.rb
CHANGED
@@ -92,5 +92,10 @@ module Pay
|
|
92
92
|
# If these match, consider it a generic trial
|
93
93
|
subscription.trial_ends_at == subscription.ends_at
|
94
94
|
end
|
95
|
+
|
96
|
+
# Attempts to pay all past_due subscriptions
|
97
|
+
def retry_past_due_subscriptions!
|
98
|
+
subscriptions.past_due.each(&:retry_failed_payment)
|
99
|
+
end
|
95
100
|
end
|
96
101
|
end
|
@@ -8,14 +8,16 @@ module Pay
|
|
8
8
|
|
9
9
|
# Scopes
|
10
10
|
scope :for_name, ->(name) { where(name: name) }
|
11
|
-
scope :on_trial, -> { where.not(trial_ends_at: nil).where("#{table_name}.trial_ends_at > ?", Time.
|
11
|
+
scope :on_trial, -> { where.not(trial_ends_at: nil).where("#{table_name}.trial_ends_at > ?", Time.current) }
|
12
12
|
scope :cancelled, -> { where.not(ends_at: nil) }
|
13
|
-
scope :on_grace_period, -> { cancelled.where("#{table_name}.ends_at > ?", Time.
|
14
|
-
|
15
|
-
|
16
|
-
scope :
|
13
|
+
scope :on_grace_period, -> { cancelled.where("#{table_name}.ends_at > ?", Time.current) }
|
14
|
+
scope :active, -> { where(status: ["trialing", "active"], ends_at: nil).pause_not_started.or(on_grace_period).or(on_trial) }
|
15
|
+
scope :paused, -> { where(status: "paused").or(where("pause_starts_at <= ?", Time.current)) }
|
16
|
+
scope :pause_not_started, -> { where("pause_starts_at IS NULL OR pause_starts_at > ?", Time.current) }
|
17
|
+
scope :active_or_paused, -> { active.or(paused) }
|
17
18
|
scope :incomplete, -> { where(status: :incomplete) }
|
18
19
|
scope :past_due, -> { where(status: :past_due) }
|
20
|
+
scope :metered, -> { where(metered: true) }
|
19
21
|
scope :with_active_customer, -> { joins(:customer).merge(Customer.active) }
|
20
22
|
scope :with_deleted_customer, -> { joins(:customer).merge(Customer.deleted) }
|
21
23
|
|
@@ -24,12 +26,8 @@ module Pay
|
|
24
26
|
|
25
27
|
store_accessor :data, :paddle_update_url
|
26
28
|
store_accessor :data, :paddle_cancel_url
|
27
|
-
store_accessor :data, :paddle_paused_from
|
28
29
|
store_accessor :data, :stripe_account
|
29
|
-
store_accessor :data, :metered
|
30
30
|
store_accessor :data, :subscription_items
|
31
|
-
store_accessor :data, :pause_behavior
|
32
|
-
store_accessor :data, :pause_resumes_at
|
33
31
|
|
34
32
|
attribute :prorate, :boolean, default: true
|
35
33
|
|
@@ -51,36 +49,6 @@ module Pay
|
|
51
49
|
scope processor_name, -> { joins(:customer).where(pay_customers: {processor: processor_name}) }
|
52
50
|
end
|
53
51
|
|
54
|
-
def self.active_without_paused
|
55
|
-
case Pay::Adapter.current_adapter
|
56
|
-
when "postgresql", "postgis"
|
57
|
-
active.where("data->>'pause_behavior' IS NULL AND status != 'paused'")
|
58
|
-
when "mysql2"
|
59
|
-
active.where("data->>'$.pause_behavior' IS NULL AND status != 'paused'")
|
60
|
-
when "sqlite3"
|
61
|
-
# sqlite 3.38 supports ->> syntax, however, sqlite 3.37 is what ships with Ubuntu 22.04.
|
62
|
-
active.where("json_extract(data, '$.pause_behavior') IS NULL AND status != 'paused'")
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
def self.with_metered_items
|
67
|
-
case Pay::Adapter.current_adapter
|
68
|
-
when "sqlite3"
|
69
|
-
where("json_extract(data, '$.\"metered\"') = true")
|
70
|
-
# For SQLite 3.38+ we could use the arrows
|
71
|
-
# where("data->'metered' = ?", "true")
|
72
|
-
when "mysql2"
|
73
|
-
where("data->'$.\"metered\"' = true")
|
74
|
-
when "postgresql", "postgis"
|
75
|
-
# Single quotes are important for json keys apparently
|
76
|
-
where("data->>'metered' = 'true'")
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
def metered_items?
|
81
|
-
!!metered
|
82
|
-
end
|
83
|
-
|
84
52
|
def self.find_by_processor_and_id(processor, processor_id)
|
85
53
|
joins(:customer).find_by(processor_id: processor_id, pay_customers: {processor: processor})
|
86
54
|
end
|
@@ -93,8 +61,9 @@ module Pay
|
|
93
61
|
@payment_processor ||= self.class.pay_processor_for(customer.processor).new(self)
|
94
62
|
end
|
95
63
|
|
96
|
-
def sync!
|
97
|
-
self.class.pay_processor_for(customer.processor).sync(processor_id)
|
64
|
+
def sync!(**options)
|
65
|
+
self.class.pay_processor_for(customer.processor).sync(processor_id, **options)
|
66
|
+
reload
|
98
67
|
end
|
99
68
|
|
100
69
|
def no_prorate
|
@@ -109,8 +78,16 @@ module Pay
|
|
109
78
|
fake_processor? && trial_ends_at?
|
110
79
|
end
|
111
80
|
|
81
|
+
def has_trial?
|
82
|
+
trial_ends_at?
|
83
|
+
end
|
84
|
+
|
112
85
|
def on_trial?
|
113
|
-
trial_ends_at? && Time.
|
86
|
+
trial_ends_at? && trial_ends_at.after?(Time.current)
|
87
|
+
end
|
88
|
+
|
89
|
+
def trial_ended?
|
90
|
+
trial_ends_at? && trial_ends_at.before?(Time.current)
|
114
91
|
end
|
115
92
|
|
116
93
|
def canceled?
|
@@ -121,8 +98,21 @@ module Pay
|
|
121
98
|
canceled?
|
122
99
|
end
|
123
100
|
|
101
|
+
def on_grace_period?
|
102
|
+
(ends_at? && Time.current < ends_at) ||
|
103
|
+
((status == "paused" || pause_behavior == "void") && will_pause?)
|
104
|
+
end
|
105
|
+
|
106
|
+
def will_pause?
|
107
|
+
pause_starts_at? && Time.current < pause_starts_at
|
108
|
+
end
|
109
|
+
|
110
|
+
def pause_active?
|
111
|
+
(status == "paused" || pause_behavior == "void") && (pause_starts_at.nil? || pause_starts_at >= Time.current)
|
112
|
+
end
|
113
|
+
|
124
114
|
def active?
|
125
|
-
["trialing", "active"
|
115
|
+
["trialing", "active"].include?(status) && (!(canceled? || paused?) || on_trial? || on_grace_period?)
|
126
116
|
end
|
127
117
|
|
128
118
|
def past_due?
|
@@ -137,8 +127,8 @@ module Pay
|
|
137
127
|
past_due? || incomplete?
|
138
128
|
end
|
139
129
|
|
140
|
-
def change_quantity(quantity)
|
141
|
-
payment_processor.change_quantity(quantity)
|
130
|
+
def change_quantity(quantity, **options)
|
131
|
+
payment_processor.change_quantity(quantity, **options)
|
142
132
|
update(quantity: quantity)
|
143
133
|
end
|
144
134
|
|
@@ -148,10 +138,9 @@ module Pay
|
|
148
138
|
self
|
149
139
|
end
|
150
140
|
|
151
|
-
def swap(plan)
|
141
|
+
def swap(plan, **options)
|
152
142
|
raise ArgumentError, "plan must be a string. Got `#{plan.inspect}` instead." unless plan.is_a?(String)
|
153
|
-
payment_processor.swap(plan)
|
154
|
-
update(processor_plan: plan, ends_at: nil, status: :active)
|
143
|
+
payment_processor.swap(plan, **options)
|
155
144
|
end
|
156
145
|
|
157
146
|
def swap_and_invoice(plan)
|
@@ -167,18 +156,6 @@ module Pay
|
|
167
156
|
processor_subscription(expand: ["latest_invoice.payment_intent"]).latest_invoice.payment_intent
|
168
157
|
end
|
169
158
|
|
170
|
-
def paddle_paused_from
|
171
|
-
if (timestamp = super)
|
172
|
-
Time.zone.parse(timestamp)
|
173
|
-
end
|
174
|
-
end
|
175
|
-
|
176
|
-
def pause_resumes_at
|
177
|
-
if (resumes_at = super)
|
178
|
-
Time.zone.parse(resumes_at)
|
179
|
-
end
|
180
|
-
end
|
181
|
-
|
182
159
|
private
|
183
160
|
|
184
161
|
def cancel_if_active
|
@@ -0,0 +1,6 @@
|
|
1
|
+
<h3>Your <%= Pay.business_name %> trial has ended</h3>
|
2
|
+
<p>This is just a friendly reminder that your <%= Pay.business_name %> trial has ended.</p>
|
3
|
+
|
4
|
+
<p>You may manage your subscription via your account. If you have any questions, please hit reply and let us know.</p>
|
5
|
+
|
6
|
+
<p>- The <%= Pay.business_name %> Team</p>
|
@@ -0,0 +1,6 @@
|
|
1
|
+
<h3>Your <%= Pay.business_name %> trial is ending soon</h3>
|
2
|
+
<p>This is just a friendly reminder that your <%= Pay.business_name %> trial will be ending soon.</p>
|
3
|
+
|
4
|
+
<p>You may manage your subscription via your account. If you have any questions, please hit reply and let us know.</p>
|
5
|
+
|
6
|
+
<p>- The <%= Pay.business_name %> Team</p>
|
data/config/locales/en.yml
CHANGED
@@ -39,14 +39,22 @@ class CreatePayTables < ActiveRecord::Migration[6.0]
|
|
39
39
|
t.string :processor_plan, null: false
|
40
40
|
t.integer :quantity, default: 1, null: false
|
41
41
|
t.string :status, null: false
|
42
|
+
t.datetime :current_period_start
|
43
|
+
t.datetime :current_period_end
|
42
44
|
t.datetime :trial_ends_at
|
43
45
|
t.datetime :ends_at
|
46
|
+
t.boolean :metered
|
47
|
+
t.string :pause_behavior
|
48
|
+
t.datetime :pause_starts_at
|
49
|
+
t.datetime :pause_resumes_at
|
44
50
|
t.decimal :application_fee_percent, precision: 8, scale: 2
|
45
51
|
t.public_send Pay::Adapter.json_column_type, :metadata
|
46
52
|
t.public_send Pay::Adapter.json_column_type, :data
|
47
53
|
t.timestamps
|
48
54
|
end
|
49
55
|
add_index :pay_subscriptions, [:customer_id, :processor_id], unique: true
|
56
|
+
add_index :pay_subscriptions, [:metered]
|
57
|
+
add_index :pay_subscriptions, [:pause_starts_at]
|
50
58
|
|
51
59
|
create_table :pay_charges do |t|
|
52
60
|
t.belongs_to :customer, foreign_key: {to_table: :pay_customers}, null: false, index: false
|
@@ -4,8 +4,9 @@ module Pay
|
|
4
4
|
attr_reader :pay_subscription
|
5
5
|
|
6
6
|
delegate :active?,
|
7
|
-
:customer,
|
8
7
|
:canceled?,
|
8
|
+
:on_grace_period?,
|
9
|
+
:customer,
|
9
10
|
:ends_at,
|
10
11
|
:name,
|
11
12
|
:on_trial?,
|
@@ -55,6 +56,10 @@ module Pay
|
|
55
56
|
raise Pay::Braintree::Error, e
|
56
57
|
end
|
57
58
|
|
59
|
+
def change_quantity(quantity, **options)
|
60
|
+
raise NotImplementedError, "Braintree does not support setting quantity on subscriptions"
|
61
|
+
end
|
62
|
+
|
58
63
|
def on_grace_period?
|
59
64
|
canceled? && Time.current < ends_at
|
60
65
|
end
|
@@ -96,7 +101,7 @@ module Pay
|
|
96
101
|
raise Pay::Braintree::Error, e
|
97
102
|
end
|
98
103
|
|
99
|
-
def swap(plan)
|
104
|
+
def swap(plan, **options)
|
100
105
|
raise ArgumentError, "plan must be a string" unless plan.is_a?(String)
|
101
106
|
|
102
107
|
if on_grace_period? && processor_plan == plan
|
@@ -128,10 +133,25 @@ module Pay
|
|
128
133
|
}
|
129
134
|
})
|
130
135
|
raise Error, "Braintree failed to swap plans: #{result.message}" unless result.success?
|
136
|
+
|
137
|
+
pay_subscription.update(processor_plan: plan, ends_at: nil, status: :active)
|
131
138
|
rescue ::Braintree::BraintreeError => e
|
132
139
|
raise Pay::Braintree::Error, e
|
133
140
|
end
|
134
141
|
|
142
|
+
# Retries the latest invoice for a Past Due subscription
|
143
|
+
def retry_failed_payment
|
144
|
+
result = gateway.subscription.retry_charge(
|
145
|
+
processor_id,
|
146
|
+
nil, # amount if different
|
147
|
+
true # submit for settlement
|
148
|
+
)
|
149
|
+
|
150
|
+
if result.success?
|
151
|
+
pay_subscription.update(status: :active)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
135
155
|
private
|
136
156
|
|
137
157
|
def gateway
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Pay
|
2
|
+
module FakeProcessor
|
3
|
+
class Merchant
|
4
|
+
attr_reader :pay_merchant
|
5
|
+
delegate :processor_id, to: :pay_merchant
|
6
|
+
|
7
|
+
def initialize(pay_merchant)
|
8
|
+
@pay_merchant = pay_merchant
|
9
|
+
end
|
10
|
+
|
11
|
+
def create_account(**options)
|
12
|
+
fake_account = Struct.new(:id).new("fake_account_id")
|
13
|
+
pay_merchant.update(processor_id: fake_account.id)
|
14
|
+
fake_account
|
15
|
+
end
|
16
|
+
|
17
|
+
def account_link(refresh_url:, return_url:, type: "account_onboarding", **options)
|
18
|
+
Struct.new(:url).new("/fake_processor/account_link")
|
19
|
+
end
|
20
|
+
|
21
|
+
def login_link(**options)
|
22
|
+
Struct.new(:url).new("/fake_processor/login_link")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -3,9 +3,11 @@ module Pay
|
|
3
3
|
class Subscription
|
4
4
|
attr_reader :pay_subscription
|
5
5
|
|
6
|
-
delegate :
|
7
|
-
:
|
6
|
+
delegate :active?,
|
7
|
+
:canceled?,
|
8
|
+
:on_grace_period?,
|
8
9
|
:on_trial?,
|
10
|
+
:ends_at,
|
9
11
|
:owner,
|
10
12
|
:processor_subscription,
|
11
13
|
:processor_id,
|
@@ -42,6 +44,10 @@ module Pay
|
|
42
44
|
)
|
43
45
|
end
|
44
46
|
|
47
|
+
def change_quantity(quantity, **options)
|
48
|
+
pay_subscription.update(quantity: quantity)
|
49
|
+
end
|
50
|
+
|
45
51
|
def on_grace_period?
|
46
52
|
canceled? && Time.current < ends_at
|
47
53
|
end
|
@@ -60,10 +66,13 @@ module Pay
|
|
60
66
|
end
|
61
67
|
end
|
62
68
|
|
63
|
-
def swap(plan)
|
69
|
+
def swap(plan, **options)
|
70
|
+
pay_subscription.update(processor_plan: plan, ends_at: nil, status: :active)
|
64
71
|
end
|
65
72
|
|
66
|
-
|
73
|
+
# Retries the latest invoice for a Past Due subscription
|
74
|
+
def retry_failed_payment
|
75
|
+
pay_subscription.update(status: :active)
|
67
76
|
end
|
68
77
|
end
|
69
78
|
end
|
data/lib/pay/fake_processor.rb
CHANGED
@@ -5,11 +5,12 @@ module Pay
|
|
5
5
|
|
6
6
|
delegate :active?,
|
7
7
|
:canceled?,
|
8
|
+
:on_grace_period?,
|
9
|
+
:on_trial?,
|
8
10
|
:ends_at,
|
9
11
|
:name,
|
10
|
-
:on_trial?,
|
11
12
|
:owner,
|
12
|
-
:
|
13
|
+
:pause_starts_at,
|
13
14
|
:processor_id,
|
14
15
|
:processor_plan,
|
15
16
|
:processor_subscription,
|
@@ -78,7 +79,7 @@ module Pay
|
|
78
79
|
ends_at = if on_trial?
|
79
80
|
trial_ends_at
|
80
81
|
elsif paused?
|
81
|
-
|
82
|
+
pause_starts_at
|
82
83
|
else
|
83
84
|
processor_subscription.next_payment&.fetch(:date) || Time.current
|
84
85
|
end
|
@@ -102,6 +103,10 @@ module Pay
|
|
102
103
|
raise Pay::Paddle::Error, e
|
103
104
|
end
|
104
105
|
|
106
|
+
def change_quantity(quantity, **options)
|
107
|
+
raise NotImplementedError, "Paddle does not support setting quantity on subscriptions"
|
108
|
+
end
|
109
|
+
|
105
110
|
def on_grace_period?
|
106
111
|
canceled? && Time.current < ends_at || paused? && Time.current < paddle_paused_from
|
107
112
|
end
|
@@ -113,7 +118,7 @@ module Pay
|
|
113
118
|
def pause
|
114
119
|
attributes = {pause: true}
|
115
120
|
response = PaddlePay::Subscription::User.update(processor_id, attributes)
|
116
|
-
pay_subscription.update(status: :paused,
|
121
|
+
pay_subscription.update(status: :paused, pause_starts_at: Time.zone.parse(response.dig(:next_payment, :date)))
|
117
122
|
rescue ::PaddlePay::PaddlePayError => e
|
118
123
|
raise Pay::Paddle::Error, e
|
119
124
|
end
|
@@ -125,20 +130,26 @@ module Pay
|
|
125
130
|
|
126
131
|
attributes = {pause: false}
|
127
132
|
PaddlePay::Subscription::User.update(processor_id, attributes)
|
128
|
-
pay_subscription.update(status: :active,
|
133
|
+
pay_subscription.update(status: :active, pause_starts_at: nil)
|
129
134
|
rescue ::PaddlePay::PaddlePayError => e
|
130
135
|
raise Pay::Paddle::Error, e
|
131
136
|
end
|
132
137
|
|
133
|
-
def swap(plan)
|
138
|
+
def swap(plan, **options)
|
134
139
|
raise ArgumentError, "plan must be a string" unless plan.is_a?(String)
|
135
140
|
|
136
141
|
attributes = {plan_id: plan, prorate: prorate}
|
137
142
|
attributes[:quantity] = quantity if quantity?
|
138
143
|
PaddlePay::Subscription::User.update(processor_id, attributes)
|
144
|
+
|
145
|
+
pay_subscription.update(processor_plan: plan, ends_at: nil, status: :active)
|
139
146
|
rescue ::PaddlePay::PaddlePayError => e
|
140
147
|
raise Pay::Paddle::Error, e
|
141
148
|
end
|
149
|
+
|
150
|
+
# Retries the latest invoice for a Past Due subscription
|
151
|
+
def retry_failed_payment
|
152
|
+
end
|
142
153
|
end
|
143
154
|
end
|
144
155
|
end
|
@@ -16,7 +16,7 @@ module Pay
|
|
16
16
|
pay_subscription.trial_ends_at = Time.zone.parse(event["next_bill_date"])
|
17
17
|
when "active"
|
18
18
|
pay_subscription.status = "active"
|
19
|
-
pay_subscription.
|
19
|
+
pay_subscription.pause_starts_at = Time.zone.parse(event["paused_from"]) if event["paused_from"].present?
|
20
20
|
else
|
21
21
|
pay_subscription.status = event["status"]
|
22
22
|
end
|
data/lib/pay/stripe/billable.rb
CHANGED
@@ -57,10 +57,7 @@ module Pay
|
|
57
57
|
end
|
58
58
|
|
59
59
|
if payment_method_token?
|
60
|
-
|
61
|
-
pay_payment_method = save_payment_method(payment_method, default: false)
|
62
|
-
pay_payment_method.make_default!
|
63
|
-
|
60
|
+
add_payment_method(payment_method_token, default: true)
|
64
61
|
pay_customer.payment_method_token = nil
|
65
62
|
end
|
66
63
|
|
@@ -89,13 +86,14 @@ module Pay
|
|
89
86
|
confirm: true,
|
90
87
|
currency: "usd",
|
91
88
|
customer: processor_id,
|
89
|
+
expand: ["latest_charge.refunds"],
|
92
90
|
payment_method: payment_method&.processor_id
|
93
91
|
}.merge(options)
|
94
92
|
|
95
93
|
payment_intent = ::Stripe::PaymentIntent.create(args, stripe_options)
|
96
94
|
Pay::Payment.new(payment_intent).validate
|
97
95
|
|
98
|
-
charge = payment_intent.
|
96
|
+
charge = payment_intent.latest_charge
|
99
97
|
Pay::Stripe::Charge.sync(charge.id, object: charge)
|
100
98
|
rescue ::Stripe::StripeError => e
|
101
99
|
raise Pay::Stripe::Error, e
|
@@ -105,8 +103,7 @@ module Pay
|
|
105
103
|
quantity = options.delete(:quantity)
|
106
104
|
opts = {
|
107
105
|
expand: ["pending_setup_intent", "latest_invoice.payment_intent", "latest_invoice.charge"],
|
108
|
-
items: [plan: plan, quantity: quantity]
|
109
|
-
off_session: true
|
106
|
+
items: [plan: plan, quantity: quantity]
|
110
107
|
}.merge(options)
|
111
108
|
|
112
109
|
# Load the Stripe customer to verify it exists and update payment method if needed
|
data/lib/pay/stripe/charge.rb
CHANGED
@@ -14,7 +14,7 @@ module Pay
|
|
14
14
|
|
15
15
|
def self.sync(charge_id, object: nil, stripe_account: nil, try: 0, retries: 1)
|
16
16
|
# Skip loading the latest charge details from the API if we already have it
|
17
|
-
object ||= ::Stripe::Charge.retrieve({id: charge_id, expand: ["invoice.total_discount_amounts.discount", "invoice.total_tax_amounts.tax_rate"]}, {stripe_account: stripe_account}.compact)
|
17
|
+
object ||= ::Stripe::Charge.retrieve({id: charge_id, expand: ["invoice.total_discount_amounts.discount", "invoice.total_tax_amounts.tax_rate", "refunds"]}, {stripe_account: stripe_account}.compact)
|
18
18
|
|
19
19
|
# Ignore charges without a Customer
|
20
20
|
return if object.customer.blank?
|
@@ -94,7 +94,7 @@ module Pay
|
|
94
94
|
else
|
95
95
|
pay_customer.charges.create!(attrs.merge(processor_id: object.id))
|
96
96
|
end
|
97
|
-
rescue ActiveRecord::RecordInvalid
|
97
|
+
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
|
98
98
|
try += 1
|
99
99
|
if try <= retries
|
100
100
|
sleep 0.1
|
@@ -9,6 +9,23 @@ module Pay
|
|
9
9
|
@pay_payment_method = pay_payment_method
|
10
10
|
end
|
11
11
|
|
12
|
+
# Syncs a PaymentIntent's payment method to the database
|
13
|
+
def self.sync_payment_intent(id, stripe_account: nil)
|
14
|
+
payment_intent = ::Stripe::PaymentIntent.retrieve({id: id, expand: ["payment_method"]}, {stripe_account: stripe_account}.compact)
|
15
|
+
payment_method = payment_intent.payment_method
|
16
|
+
return unless payment_method
|
17
|
+
Pay::Stripe::PaymentMethod.sync(payment_method.id, object: payment_method)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Syncs a SetupIntent's payment method to the database
|
21
|
+
def self.sync_setup_intent(id, stripe_account: nil)
|
22
|
+
setup_intent = ::Stripe::SetupIntent.retrieve({id: id, expand: ["payment_method"]}, {stripe_account: stripe_account}.compact)
|
23
|
+
payment_method = setup_intent.payment_method
|
24
|
+
return unless payment_method
|
25
|
+
Pay::Stripe::PaymentMethod.sync(payment_method.id, object: payment_method)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Syncs PaymentMethod objects from Stripe
|
12
29
|
def self.sync(id, object: nil, stripe_account: nil, try: 0, retries: 1)
|
13
30
|
object ||= ::Stripe::PaymentMethod.retrieve(id, {stripe_account: stripe_account}.compact)
|
14
31
|
|
@@ -24,6 +41,14 @@ module Pay
|
|
24
41
|
pay_payment_method = pay_customer.payment_methods.where(processor_id: object.id).first_or_initialize
|
25
42
|
pay_payment_method.update!(attributes)
|
26
43
|
pay_payment_method
|
44
|
+
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
|
45
|
+
try += 1
|
46
|
+
if try <= retries
|
47
|
+
sleep 0.1
|
48
|
+
retry
|
49
|
+
else
|
50
|
+
raise
|
51
|
+
end
|
27
52
|
end
|
28
53
|
|
29
54
|
# Extracts payment method details from a Stripe::PaymentMethod object
|
@@ -6,6 +6,7 @@ module Pay
|
|
6
6
|
|
7
7
|
delegate :active?,
|
8
8
|
:canceled?,
|
9
|
+
:on_grace_period?,
|
9
10
|
:ends_at,
|
10
11
|
:name,
|
11
12
|
:on_trial?,
|
@@ -21,6 +22,8 @@ module Pay
|
|
21
22
|
:trial_ends_at,
|
22
23
|
:pause_behavior,
|
23
24
|
:pause_resumes_at,
|
25
|
+
:current_period_start,
|
26
|
+
:current_period_end,
|
24
27
|
to: :pay_subscription
|
25
28
|
|
26
29
|
def self.sync(subscription_id, object: nil, name: nil, stripe_account: nil, try: 0, retries: 1)
|
@@ -32,6 +35,7 @@ module Pay
|
|
32
35
|
|
33
36
|
attributes = {
|
34
37
|
application_fee_percent: object.application_fee_percent,
|
38
|
+
created_at: Time.at(object.created),
|
35
39
|
processor_plan: object.items.first.price.id,
|
36
40
|
quantity: object.items.first.try(:quantity) || 0,
|
37
41
|
status: object.status,
|
@@ -40,12 +44,20 @@ module Pay
|
|
40
44
|
subscription_items: [],
|
41
45
|
metered: false,
|
42
46
|
pause_behavior: object.pause_collection&.behavior,
|
43
|
-
pause_resumes_at: (object.pause_collection&.resumes_at ? Time.at(object.pause_collection&.resumes_at) : nil)
|
47
|
+
pause_resumes_at: (object.pause_collection&.resumes_at ? Time.at(object.pause_collection&.resumes_at) : nil),
|
48
|
+
current_period_start: (object.current_period_start ? Time.at(object.current_period_start) : nil),
|
49
|
+
current_period_end: (object.current_period_end ? Time.at(object.current_period_end) : nil)
|
44
50
|
}
|
45
51
|
|
46
|
-
# Subscriptions that have ended should have their trial ended at the
|
52
|
+
# Subscriptions that have ended should have their trial ended at the
|
53
|
+
# same time if they were still on trial (if you cancel a
|
54
|
+
# subscription, your are cancelling your trial as well at the same
|
55
|
+
# instant). This avoids canceled subscriptions responding `true`
|
56
|
+
# to #on_trial? due to the `trial_ends_at` being left set in the
|
57
|
+
# future.
|
47
58
|
if object.trial_end
|
48
|
-
|
59
|
+
trial_ended_at = [object.ended_at, object.trial_end].compact.min
|
60
|
+
attributes[:trial_ends_at] = Time.at(trial_ended_at)
|
49
61
|
end
|
50
62
|
|
51
63
|
# Record subscription items to db
|
@@ -71,11 +83,18 @@ module Pay
|
|
71
83
|
# Update or create the subscription
|
72
84
|
pay_subscription = pay_customer.subscriptions.find_by(processor_id: object.id)
|
73
85
|
if pay_subscription
|
86
|
+
# If pause behavior is changing to `void`, record the pause start date
|
87
|
+
# Any other pause status (or no pause at all) should have nil for start
|
88
|
+
if pay_subscription.pause_behavior != attributes[:pause_behavior]
|
89
|
+
attributes[:pause_starts_at] = if attributes[:pause_behavior] == "void"
|
90
|
+
Time.at(object.current_period_end)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
74
94
|
pay_subscription.with_lock { pay_subscription.update!(attributes) }
|
75
95
|
else
|
76
96
|
# Allow setting the subscription name in metadata, otherwise use the default
|
77
97
|
name ||= object.metadata["pay_name"] || Pay.default_product_name
|
78
|
-
|
79
98
|
pay_subscription = pay_customer.subscriptions.create!(attributes.merge(name: name, processor_id: object.id))
|
80
99
|
end
|
81
100
|
|
@@ -88,7 +107,7 @@ module Pay
|
|
88
107
|
end
|
89
108
|
|
90
109
|
pay_subscription
|
91
|
-
rescue ActiveRecord::RecordInvalid
|
110
|
+
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
|
92
111
|
try += 1
|
93
112
|
if try <= retries
|
94
113
|
sleep 0.1
|
@@ -153,7 +172,7 @@ module Pay
|
|
153
172
|
# For a subscription with a single item, we can update the subscription directly if no SubscriptionItem ID is available
|
154
173
|
# Otherwise a SubscriptionItem ID is required so Stripe knows which entry to update
|
155
174
|
def change_quantity(quantity, **options)
|
156
|
-
subscription_item_id = options.
|
175
|
+
subscription_item_id = options.delete(:subscription_item_id) || subscription_items&.first&.dig("id")
|
157
176
|
if subscription_item_id
|
158
177
|
::Stripe::SubscriptionItem.update(subscription_item_id, options.merge(quantity: quantity), stripe_options)
|
159
178
|
@stripe_subscription = nil
|
@@ -165,12 +184,8 @@ module Pay
|
|
165
184
|
raise Pay::Stripe::Error, e
|
166
185
|
end
|
167
186
|
|
168
|
-
def on_grace_period?
|
169
|
-
canceled? && Time.current < ends_at
|
170
|
-
end
|
171
|
-
|
172
187
|
def paused?
|
173
|
-
pause_behavior
|
188
|
+
pause_behavior == "void"
|
174
189
|
end
|
175
190
|
|
176
191
|
# Pauses a Stripe subscription
|
@@ -179,18 +194,32 @@ module Pay
|
|
179
194
|
# pause(behavior: "keep_as_draft")
|
180
195
|
# pause(behavior: "void")
|
181
196
|
# pause(behavior: "mark_uncollectible", resumes_at: 1.month.from_now)
|
197
|
+
#
|
198
|
+
# `void` - If you can’t provide your services for a certain period of time, you can void invoices that are created by your subscriptions so that your customers aren’t charged.
|
199
|
+
# `keep_as_draft` - If you want to temporarily offer your services for free and collect payments later
|
200
|
+
# `mark_uncollectible` - If you want to offer your services for free
|
201
|
+
#
|
202
|
+
# pause_behavior of `void` is considered active until the end of the current period and not active after that. The current_period_end is stored as `pause_starts_at`
|
203
|
+
# Other pause_behaviors do not set `pause_starts_at` because they are used for offering free services
|
182
204
|
def pause(**options)
|
183
|
-
attributes = {pause_collection: options.reverse_merge(behavior: "
|
205
|
+
attributes = {pause_collection: options.reverse_merge(behavior: "void")}
|
184
206
|
@stripe_subscription = ::Stripe::Subscription.update(processor_id, attributes.merge(expand_options), stripe_options)
|
207
|
+
behavior = @stripe_subscription.pause_collection&.behavior
|
185
208
|
pay_subscription.update(
|
186
|
-
pause_behavior:
|
187
|
-
pause_resumes_at: (@stripe_subscription.pause_collection&.resumes_at ? Time.at(@stripe_subscription.pause_collection&.resumes_at) : nil)
|
209
|
+
pause_behavior: behavior,
|
210
|
+
pause_resumes_at: (@stripe_subscription.pause_collection&.resumes_at ? Time.at(@stripe_subscription.pause_collection&.resumes_at) : nil),
|
211
|
+
pause_starts_at: ((behavior == "void") ? Time.at(@stripe_subscription.current_period_end) : nil)
|
188
212
|
)
|
189
213
|
end
|
190
214
|
|
215
|
+
# Unpauses a subscription
|
191
216
|
def unpause
|
192
217
|
@stripe_subscription = ::Stripe::Subscription.update(processor_id, {pause_collection: nil}.merge(expand_options), stripe_options)
|
193
|
-
pay_subscription.update(
|
218
|
+
pay_subscription.update(
|
219
|
+
pause_behavior: nil,
|
220
|
+
pause_resumes_at: nil,
|
221
|
+
pause_starts_at: nil
|
222
|
+
)
|
194
223
|
end
|
195
224
|
|
196
225
|
def resume
|
@@ -215,20 +244,24 @@ module Pay
|
|
215
244
|
raise Pay::Stripe::Error, e
|
216
245
|
end
|
217
246
|
|
218
|
-
def swap(plan)
|
247
|
+
def swap(plan, **options)
|
219
248
|
raise ArgumentError, "plan must be a string" unless plan.is_a?(String)
|
220
249
|
|
250
|
+
proration_behavior = options.delete(:proration_behavior) || (prorate ? "always_invoice" : "none")
|
251
|
+
|
221
252
|
@stripe_subscription = ::Stripe::Subscription.update(
|
222
253
|
processor_id,
|
223
254
|
{
|
224
255
|
cancel_at_period_end: false,
|
225
256
|
plan: plan,
|
226
|
-
proration_behavior:
|
257
|
+
proration_behavior: proration_behavior,
|
227
258
|
trial_end: (on_trial? ? trial_ends_at.to_i : "now"),
|
228
259
|
quantity: quantity
|
229
260
|
}.merge(expand_options),
|
230
261
|
stripe_options
|
231
262
|
)
|
263
|
+
|
264
|
+
pay_subscription.sync!(object: @stripe_subscription)
|
232
265
|
rescue ::Stripe::StripeError => e
|
233
266
|
raise Pay::Stripe::Error, e
|
234
267
|
end
|
@@ -262,6 +295,11 @@ module Pay
|
|
262
295
|
::Stripe::Invoice.upcoming(options.merge(subscription: processor_id), stripe_options)
|
263
296
|
end
|
264
297
|
|
298
|
+
# Retries the latest invoice for a Past Due subscription
|
299
|
+
def retry_failed_payment
|
300
|
+
subscription.latest_invoice.pay
|
301
|
+
end
|
302
|
+
|
265
303
|
private
|
266
304
|
|
267
305
|
# Options for Stripe requests
|
@@ -9,9 +9,7 @@ module Pay
|
|
9
9
|
|
10
10
|
def call(event)
|
11
11
|
object = event.data.object
|
12
|
-
object.
|
13
|
-
Pay::Stripe::Charge.sync(charge.id, stripe_account: event.try(:account))
|
14
|
-
end
|
12
|
+
Pay::Stripe::Charge.sync(object.latest_charge, stripe_account: event.try(:account))
|
15
13
|
end
|
16
14
|
end
|
17
15
|
end
|
@@ -4,10 +4,7 @@ module Pay
|
|
4
4
|
class PaymentMethodAttached
|
5
5
|
def call(event)
|
6
6
|
object = event.data.object
|
7
|
-
|
8
|
-
return unless pay_customer
|
9
|
-
|
10
|
-
pay_customer.save_payment_method(object, default: false)
|
7
|
+
Pay::Stripe::PaymentMethod.sync(object.id, stripe_account: event.try(:account))
|
11
8
|
end
|
12
9
|
end
|
13
10
|
end
|
@@ -4,13 +4,7 @@ module Pay
|
|
4
4
|
class PaymentMethodUpdated
|
5
5
|
def call(event)
|
6
6
|
object = event.data.object
|
7
|
-
|
8
7
|
if object.customer
|
9
|
-
pay_customer = Pay::Customer.find_by(processor: :stripe, processor_id: object.customer)
|
10
|
-
|
11
|
-
# Couldn't find user, we can skip
|
12
|
-
return unless pay_customer.present?
|
13
|
-
|
14
8
|
Pay::Stripe::PaymentMethod.sync(object.id, stripe_account: event.try(:account))
|
15
9
|
else
|
16
10
|
# If customer was removed, we should delete the payment method if it exists
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Pay
|
2
|
+
module Stripe
|
3
|
+
module Webhooks
|
4
|
+
class SubscriptionTrialWillEnd
|
5
|
+
def call(event)
|
6
|
+
object = event.data.object
|
7
|
+
|
8
|
+
pay_subscription = Pay::Subscription.find_by_processor_and_id(:stripe, object.id)
|
9
|
+
return if pay_subscription.nil?
|
10
|
+
|
11
|
+
pay_subscription.sync!
|
12
|
+
|
13
|
+
pay_user_mailer = Pay.mailer.with(pay_customer: pay_subscription.customer, pay_subscription: pay_subscription)
|
14
|
+
|
15
|
+
if Pay.send_email?(:subscription_trial_will_end, pay_subscription) && pay_subscription.on_trial?
|
16
|
+
pay_user_mailer.subscription_trial_will_end.deliver_later
|
17
|
+
elsif Pay.send_email?(:subscription_trial_ended, pay_subscription) && pay_subscription.trial_ended?
|
18
|
+
pay_user_mailer.subscription_trial_ended.deliver_later
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/pay/stripe.rb
CHANGED
@@ -24,6 +24,7 @@ module Pay
|
|
24
24
|
autoload :SubscriptionDeleted, "pay/stripe/webhooks/subscription_deleted"
|
25
25
|
autoload :SubscriptionRenewing, "pay/stripe/webhooks/subscription_renewing"
|
26
26
|
autoload :SubscriptionUpdated, "pay/stripe/webhooks/subscription_updated"
|
27
|
+
autoload :SubscriptionTrialWillEnd, "pay/stripe/webhooks/subscription_trial_will_end"
|
27
28
|
end
|
28
29
|
|
29
30
|
extend Env
|
@@ -31,12 +32,12 @@ module Pay
|
|
31
32
|
def self.enabled?
|
32
33
|
return false unless Pay.enabled_processors.include?(:stripe) && defined?(::Stripe)
|
33
34
|
|
34
|
-
Pay::Engine.version_matches?(required: "~>
|
35
|
+
Pay::Engine.version_matches?(required: "~> 8", current: ::Stripe::VERSION) || (raise "[Pay] stripe gem must be version ~> 8")
|
35
36
|
end
|
36
37
|
|
37
38
|
def self.setup
|
38
39
|
::Stripe.api_key = private_key
|
39
|
-
::Stripe.api_version = "2022-
|
40
|
+
::Stripe.api_version = "2022-11-15"
|
40
41
|
|
41
42
|
# Used by Stripe to identify Pay for support
|
42
43
|
::Stripe.set_app_info("PayRails", partner_id: "pp_partner_IqhY0UExnJYLxg", version: Pay::VERSION, url: "https://github.com/pay-rails/pay")
|
@@ -86,6 +87,9 @@ module Pay
|
|
86
87
|
# When a customers subscription is canceled, we want to update our records
|
87
88
|
events.subscribe "stripe.customer.subscription.deleted", Pay::Stripe::Webhooks::SubscriptionDeleted.new
|
88
89
|
|
90
|
+
# When a customers subscription trial period is 3 days from ending or ended immediately this event is fired
|
91
|
+
events.subscribe "stripe.customer.subscription.trial_will_end", Pay::Stripe::Webhooks::SubscriptionTrialWillEnd.new
|
92
|
+
|
89
93
|
# Monitor changes for customer's default card changing
|
90
94
|
events.subscribe "stripe.customer.updated", Pay::Stripe::Webhooks::CustomerUpdated.new
|
91
95
|
|
data/lib/pay/version.rb
CHANGED
data/lib/pay.rb
CHANGED
@@ -61,6 +61,8 @@ module Pay
|
|
61
61
|
@@emails.subscription_renewing = ->(pay_subscription, price) {
|
62
62
|
(price&.type == "recurring") && (price.recurring&.interval == "year")
|
63
63
|
}
|
64
|
+
@@emails.subscription_trial_will_end = true
|
65
|
+
@@emails.subscription_trial_ended = true
|
64
66
|
|
65
67
|
@@mailer = "Pay::UserMailer"
|
66
68
|
|
@@ -76,6 +78,27 @@ module Pay
|
|
76
78
|
mattr_accessor :parent_mailer
|
77
79
|
@@parent_mailer = "Pay::ApplicationMailer"
|
78
80
|
|
81
|
+
# Should return a hash of arguments for the `mail` call in UserMailer
|
82
|
+
mattr_accessor :mail_arguments
|
83
|
+
@@mail_arguments = ->(mailer, params) {
|
84
|
+
{
|
85
|
+
to: Pay.mail_to.call(mailer, params)
|
86
|
+
}
|
87
|
+
}
|
88
|
+
|
89
|
+
# Should return String or Array of email recipients
|
90
|
+
mattr_accessor :mail_to
|
91
|
+
@@mail_to = ->(mailer, params) {
|
92
|
+
if ActionMailer::Base.respond_to?(:email_address_with_name)
|
93
|
+
ActionMailer::Base.email_address_with_name(params[:pay_customer].email, params[:pay_customer].customer_name)
|
94
|
+
else
|
95
|
+
Mail::Address.new.tap do |builder|
|
96
|
+
builder.address = params[:pay_customer].email
|
97
|
+
builder.display_name = params[:pay_customer].customer_name.presence
|
98
|
+
end.to_s
|
99
|
+
end
|
100
|
+
}
|
101
|
+
|
79
102
|
def self.setup
|
80
103
|
yield self
|
81
104
|
end
|
data/lib/tasks/pay.rake
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: 6.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jason Charnes
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2022-
|
12
|
+
date: 2022-11-22 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rails
|
@@ -119,6 +119,8 @@ files:
|
|
119
119
|
- app/views/pay/user_mailer/receipt.html.erb
|
120
120
|
- app/views/pay/user_mailer/refund.html.erb
|
121
121
|
- app/views/pay/user_mailer/subscription_renewing.html.erb
|
122
|
+
- app/views/pay/user_mailer/subscription_trial_ended.html.erb
|
123
|
+
- app/views/pay/user_mailer/subscription_trial_will_end.html.erb
|
122
124
|
- config/currencies/iso.json
|
123
125
|
- config/locales/en.yml
|
124
126
|
- config/routes.rb
|
@@ -151,6 +153,7 @@ files:
|
|
151
153
|
- lib/pay/fake_processor/billable.rb
|
152
154
|
- lib/pay/fake_processor/charge.rb
|
153
155
|
- lib/pay/fake_processor/error.rb
|
156
|
+
- lib/pay/fake_processor/merchant.rb
|
154
157
|
- lib/pay/fake_processor/payment_method.rb
|
155
158
|
- lib/pay/fake_processor/subscription.rb
|
156
159
|
- lib/pay/nano_id.rb
|
@@ -191,6 +194,7 @@ files:
|
|
191
194
|
- lib/pay/stripe/webhooks/subscription_created.rb
|
192
195
|
- lib/pay/stripe/webhooks/subscription_deleted.rb
|
193
196
|
- lib/pay/stripe/webhooks/subscription_renewing.rb
|
197
|
+
- lib/pay/stripe/webhooks/subscription_trial_will_end.rb
|
194
198
|
- lib/pay/stripe/webhooks/subscription_updated.rb
|
195
199
|
- lib/pay/version.rb
|
196
200
|
- lib/pay/webhooks.rb
|