pay 7.1.1 → 7.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +5 -1
- data/app/models/pay/subscription.rb +2 -2
- data/lib/pay/fake_processor/billable.rb +3 -2
- data/lib/pay/lemon_squeezy/billable.rb +90 -0
- data/lib/pay/lemon_squeezy/charge.rb +68 -0
- data/lib/pay/lemon_squeezy/error.rb +7 -0
- data/lib/pay/lemon_squeezy/payment_method.rb +40 -0
- data/lib/pay/lemon_squeezy/subscription.rb +185 -0
- data/lib/pay/lemon_squeezy/webhooks/subscription.rb +11 -0
- data/lib/pay/lemon_squeezy/webhooks/transaction_completed.rb +11 -0
- data/lib/pay/lemon_squeezy.rb +138 -0
- data/lib/pay/paddle_billing/charge.rb +2 -2
- data/lib/pay/paddle_billing/payment_method.rb +8 -1
- data/lib/pay/paddle_billing/subscription.rb +3 -3
- data/lib/pay/stripe/subscription.rb +6 -2
- data/lib/pay/stripe.rb +1 -1
- data/lib/pay/version.rb +1 -1
- metadata +11 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 690bb7ad1078274199d3a9c3c32b5ace7fb405bf0c37f0bcabeffeb8af734902
|
4
|
+
data.tar.gz: 1e0df19aea4d907f3878450fcbb320421a3abb7da1fe0187ba7bde1878267d60
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a0767157a5d6ecc7a710e71047080b4e5d6edbdf4a27fb7f2fcfbf585558b68b43af5f483d87815668dcc4d85128f3fe6c098a5fc2899195529142a1ba3682fa
|
7
|
+
data.tar.gz: 400de12e3525fc3395ba4bf4dd8b2df158de62cb18db60a298d51acec1fe1a16fa0c32f0f3750f9f8a01f0337de9f71d61b33628e0c39848508dc818c85f5356
|
data/README.md
CHANGED
@@ -44,7 +44,7 @@ Want to add a new payment provider? Contributions are welcome.
|
|
44
44
|
* **Payment Processors**
|
45
45
|
* [Stripe](docs/stripe/1_overview.md)
|
46
46
|
* [Braintree](docs/braintree/1_overview.md)
|
47
|
-
* [Paddle](docs/
|
47
|
+
* [Paddle](docs/paddle_billing/1_overview.md)
|
48
48
|
* [Fake Processor](docs/fake_processor/1_overview.md)
|
49
49
|
* **Marketplaces**
|
50
50
|
* [Stripe Connect](docs/marketplaces/stripe_connect.md)
|
@@ -55,6 +55,10 @@ Want to add a new payment provider? Contributions are welcome.
|
|
55
55
|
|
56
56
|
If you have an issue you'd like to submit, please do so using the issue tracker in GitHub. In order for us to help you in the best way possible, please be as detailed as you can.
|
57
57
|
|
58
|
+
For those using devcontainers, if you want to test the application with different databases:
|
59
|
+
1. Uncomment the `DATABASE_URL` corresponding to the database type you wish to use in the `.devcontainer/devcontainer.json` file.
|
60
|
+
2. Rebuild the devcontainer, which will configure the application to use the selected database for your development environment.
|
61
|
+
|
58
62
|
If you'd like to open a PR please make sure the following things pass:
|
59
63
|
|
60
64
|
```ruby
|
@@ -9,11 +9,11 @@ module Pay
|
|
9
9
|
|
10
10
|
# Scopes
|
11
11
|
scope :for_name, ->(name) { where(name: name) }
|
12
|
-
scope :on_trial, -> { where("trial_ends_at > ?", Time.current) }
|
12
|
+
scope :on_trial, -> { where(status: ["trialing", "active"]).where("trial_ends_at > ?", Time.current) }
|
13
13
|
scope :canceled, -> { where.not(ends_at: nil) }
|
14
14
|
scope :cancelled, -> { canceled }
|
15
15
|
scope :on_grace_period, -> { where("#{table_name}.ends_at IS NOT NULL AND #{table_name}.ends_at > ?", Time.current) }
|
16
|
-
scope :active, -> { where(status:
|
16
|
+
scope :active, -> { where(status: "active").pause_not_started.where("#{table_name}.ends_at IS NULL OR #{table_name}.ends_at > ?", Time.current).or(on_trial) }
|
17
17
|
scope :paused, -> { where(status: "paused").or(where("pause_starts_at <= ?", Time.current)) }
|
18
18
|
scope :pause_not_started, -> { where("pause_starts_at IS NULL OR pause_starts_at > ?", Time.current) }
|
19
19
|
scope :active_or_paused, -> { active.or(paused) }
|
@@ -28,7 +28,8 @@ module Pay
|
|
28
28
|
# Make to generate a processor_id
|
29
29
|
customer
|
30
30
|
|
31
|
-
|
31
|
+
valid_attributes = options.slice(*Pay::Charge.attribute_names.map(&:to_sym))
|
32
|
+
attributes = {
|
32
33
|
processor_id: NanoId.generate,
|
33
34
|
amount: amount,
|
34
35
|
data: {
|
@@ -38,7 +39,7 @@ module Pay
|
|
38
39
|
exp_month: Date.today.month,
|
39
40
|
exp_year: Date.today.year
|
40
41
|
}
|
41
|
-
)
|
42
|
+
}.deep_merge(valid_attributes)
|
42
43
|
pay_customer.charges.create!(attributes)
|
43
44
|
end
|
44
45
|
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module Pay
|
2
|
+
module PaddleBilling
|
3
|
+
class Billable
|
4
|
+
attr_reader :pay_customer
|
5
|
+
|
6
|
+
delegate :processor_id,
|
7
|
+
:processor_id?,
|
8
|
+
:email,
|
9
|
+
:customer_name,
|
10
|
+
:card_token,
|
11
|
+
to: :pay_customer
|
12
|
+
|
13
|
+
def initialize(pay_customer)
|
14
|
+
@pay_customer = pay_customer
|
15
|
+
end
|
16
|
+
|
17
|
+
def customer_attributes
|
18
|
+
{email: email, name: customer_name}
|
19
|
+
end
|
20
|
+
|
21
|
+
# Retrieves a Paddle::Customer object
|
22
|
+
#
|
23
|
+
# Finds an existing Paddle::Customer if processor_id exists
|
24
|
+
# Creates a new Paddle::Customer using `email` and `customer_name` if empty processor_id
|
25
|
+
#
|
26
|
+
# Returns a Paddle::Customer object
|
27
|
+
def customer
|
28
|
+
if processor_id?
|
29
|
+
::Paddle::Customer.retrieve(id: processor_id)
|
30
|
+
else
|
31
|
+
sc = ::Paddle::Customer.create(email: email, name: customer_name)
|
32
|
+
pay_customer.update!(processor_id: sc.id)
|
33
|
+
sc
|
34
|
+
end
|
35
|
+
rescue ::Paddle::Error => e
|
36
|
+
raise Pay::PaddleBilling::Error, e
|
37
|
+
end
|
38
|
+
|
39
|
+
# Syncs name and email to Paddle::Customer
|
40
|
+
# You can also pass in other attributes that will be merged into the default attributes
|
41
|
+
def update_customer!(**attributes)
|
42
|
+
customer unless processor_id?
|
43
|
+
attrs = customer_attributes.merge(attributes)
|
44
|
+
::Paddle::Customer.update(id: processor_id, **attrs)
|
45
|
+
end
|
46
|
+
|
47
|
+
def charge(amount, options = {})
|
48
|
+
return Pay::Error unless options
|
49
|
+
|
50
|
+
items = options[:items]
|
51
|
+
opts = options.except(:items).merge(customer_id: processor_id)
|
52
|
+
transaction = ::Paddle::Transaction.create(items: items, **opts)
|
53
|
+
|
54
|
+
attrs = {
|
55
|
+
amount: transaction.details.totals.grand_total,
|
56
|
+
created_at: transaction.created_at,
|
57
|
+
currency: transaction.currency_code,
|
58
|
+
metadata: transaction.details.line_items&.first&.id
|
59
|
+
}
|
60
|
+
|
61
|
+
charge = pay_customer.charges.find_or_initialize_by(processor_id: transaction.id)
|
62
|
+
charge.update(attrs)
|
63
|
+
charge
|
64
|
+
rescue ::Paddle::Error => e
|
65
|
+
raise Pay::PaddleBilling::Error, e
|
66
|
+
end
|
67
|
+
|
68
|
+
def subscribe(name: Pay.default_product_name, plan: Pay.default_plan_name, **options)
|
69
|
+
# pass
|
70
|
+
end
|
71
|
+
|
72
|
+
# Paddle does not use payment method tokens. The method signature has it here
|
73
|
+
# to have a uniform API with the other payment processors.
|
74
|
+
def add_payment_method(token = nil, default: true)
|
75
|
+
Pay::PaddleBilling::PaymentMethod.sync(pay_customer: pay_customer)
|
76
|
+
end
|
77
|
+
|
78
|
+
def trial_end_date(subscription)
|
79
|
+
return unless subscription.state == "trialing"
|
80
|
+
Time.zone.parse(subscription.next_payment[:date]).end_of_day
|
81
|
+
end
|
82
|
+
|
83
|
+
def processor_subscription(subscription_id, options = {})
|
84
|
+
::Paddle::Subscription.retrieve(id: subscription_id, **options)
|
85
|
+
rescue ::Paddle::Error => e
|
86
|
+
raise Pay::PaddleBilling::Error, e
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Pay
|
2
|
+
module PaddleBilling
|
3
|
+
class Charge
|
4
|
+
attr_reader :pay_charge
|
5
|
+
|
6
|
+
delegate :processor_id, :customer, to: :pay_charge
|
7
|
+
|
8
|
+
def initialize(pay_charge)
|
9
|
+
@pay_charge = pay_charge
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.sync(charge_id, object: nil, try: 0, retries: 1)
|
13
|
+
# Skip loading the latest charge details from the API if we already have it
|
14
|
+
object ||= ::Paddle::Transaction.retrieve(id: charge_id)
|
15
|
+
|
16
|
+
# Ignore transactions that aren't completed
|
17
|
+
return unless object.status == "completed"
|
18
|
+
|
19
|
+
# Ignore charges without a Customer
|
20
|
+
return if object.customer_id.blank?
|
21
|
+
|
22
|
+
pay_customer = Pay::Customer.find_by(processor: :paddle_billing, processor_id: object.customer_id)
|
23
|
+
return unless pay_customer
|
24
|
+
|
25
|
+
# Ignore transactions that are payment method changes
|
26
|
+
# But update the customer's payment method
|
27
|
+
if object.origin == "subscription_payment_method_change"
|
28
|
+
Pay::PaddleBilling::PaymentMethod.sync(pay_customer: pay_customer, attributes: object.payments.first)
|
29
|
+
return
|
30
|
+
end
|
31
|
+
|
32
|
+
attrs = {
|
33
|
+
amount: object.details.totals.grand_total,
|
34
|
+
created_at: object.created_at,
|
35
|
+
currency: object.currency_code,
|
36
|
+
metadata: object.details.line_items&.first&.id,
|
37
|
+
subscription: pay_customer.subscriptions.find_by(processor_id: object.subscription_id)
|
38
|
+
}
|
39
|
+
|
40
|
+
if object.payment
|
41
|
+
case object.payment.method_details.type.downcase
|
42
|
+
when "card"
|
43
|
+
attrs[:payment_method_type] = "card"
|
44
|
+
attrs[:brand] = details.card.type
|
45
|
+
attrs[:exp_month] = details.card.expiry_month
|
46
|
+
attrs[:exp_year] = details.card.expiry_year
|
47
|
+
attrs[:last4] = details.card.last4
|
48
|
+
when "paypal"
|
49
|
+
attrs[:payment_method_type] = "paypal"
|
50
|
+
end
|
51
|
+
|
52
|
+
# Update customer's payment method
|
53
|
+
Pay::PaddleBilling::PaymentMethod.sync(pay_customer: pay_customer, attributes: object.payments.first)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Update or create the charge
|
57
|
+
if (pay_charge = pay_customer.charges.find_by(processor_id: object.id))
|
58
|
+
pay_charge.with_lock do
|
59
|
+
pay_charge.update!(attrs)
|
60
|
+
end
|
61
|
+
pay_charge
|
62
|
+
else
|
63
|
+
pay_customer.charges.create!(attrs.merge(processor_id: object.id))
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Pay
|
2
|
+
module PaddleBilling
|
3
|
+
class PaymentMethod
|
4
|
+
attr_reader :pay_payment_method
|
5
|
+
|
6
|
+
delegate :customer, :processor_id, to: :pay_payment_method
|
7
|
+
|
8
|
+
def self.sync(pay_customer:, attributes:)
|
9
|
+
details = attributes.method_details
|
10
|
+
attrs = {
|
11
|
+
type: details.type.downcase
|
12
|
+
}
|
13
|
+
|
14
|
+
case details.type.downcase
|
15
|
+
when "card"
|
16
|
+
attrs[:brand] = details.card.type
|
17
|
+
attrs[:last4] = details.card.last4
|
18
|
+
attrs[:exp_month] = details.card.expiry_month
|
19
|
+
attrs[:exp_year] = details.card.expiry_year
|
20
|
+
end
|
21
|
+
|
22
|
+
payment_method = pay_customer.payment_methods.find_or_initialize_by(processor_id: attributes.stored_payment_method_id)
|
23
|
+
payment_method.update!(attrs)
|
24
|
+
payment_method
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize(pay_payment_method)
|
28
|
+
@pay_payment_method = pay_payment_method
|
29
|
+
end
|
30
|
+
|
31
|
+
# Sets payment method as default
|
32
|
+
def make_default!
|
33
|
+
end
|
34
|
+
|
35
|
+
# Remove payment method
|
36
|
+
def detach
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
module Pay
|
2
|
+
module PaddleBilling
|
3
|
+
class Subscription
|
4
|
+
attr_reader :pay_subscription
|
5
|
+
|
6
|
+
delegate :active?,
|
7
|
+
:canceled?,
|
8
|
+
:on_grace_period?,
|
9
|
+
:on_trial?,
|
10
|
+
:ends_at,
|
11
|
+
:name,
|
12
|
+
:owner,
|
13
|
+
:pause_starts_at,
|
14
|
+
:pause_starts_at?,
|
15
|
+
:processor_id,
|
16
|
+
:processor_plan,
|
17
|
+
:processor_subscription,
|
18
|
+
:prorate,
|
19
|
+
:prorate?,
|
20
|
+
:quantity,
|
21
|
+
:quantity?,
|
22
|
+
:trial_ends_at,
|
23
|
+
to: :pay_subscription
|
24
|
+
|
25
|
+
def self.sync_from_transaction(transaction_id)
|
26
|
+
transaction = ::Paddle::Transaction.retrieve(id: transaction_id)
|
27
|
+
sync(transaction.subscription_id) if transaction.subscription_id
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.sync(subscription_id, object: nil, name: Pay.default_product_name)
|
31
|
+
# Passthrough is not return from this API, so we can't use that
|
32
|
+
object ||= ::Paddle::Subscription.retrieve(id: subscription_id)
|
33
|
+
|
34
|
+
pay_customer = Pay::Customer.find_by(processor: :paddle_billing, processor_id: object.customer_id)
|
35
|
+
return unless pay_customer
|
36
|
+
|
37
|
+
attributes = {
|
38
|
+
current_period_end: object.current_billing_period&.ends_at,
|
39
|
+
current_period_start: object.current_billing_period&.starts_at,
|
40
|
+
ends_at: (object.canceled_at ? Time.parse(object.canceled_at) : nil),
|
41
|
+
metadata: object.custom_data,
|
42
|
+
paddle_cancel_url: object.management_urls&.cancel,
|
43
|
+
paddle_update_url: object.management_urls&.update_payment_method,
|
44
|
+
pause_starts_at: (object.paused_at ? Time.parse(object.paused_at) : nil),
|
45
|
+
status: object.status
|
46
|
+
}
|
47
|
+
|
48
|
+
if object.items&.first
|
49
|
+
item = object.items.first
|
50
|
+
attributes[:processor_plan] = item.price.id
|
51
|
+
attributes[:quantity] = item.quantity
|
52
|
+
end
|
53
|
+
|
54
|
+
case attributes[:status]
|
55
|
+
when "canceled"
|
56
|
+
# Remove payment methods since customer cannot be reused after cancelling
|
57
|
+
Pay::PaymentMethod.where(customer_id: object.customer_id).destroy_all
|
58
|
+
when "trialing"
|
59
|
+
attributes[:trial_ends_at] = Time.parse(object.next_billed_at)
|
60
|
+
when "paused"
|
61
|
+
attributes[:pause_starts_at] = Time.parse(object.paused_at)
|
62
|
+
end
|
63
|
+
|
64
|
+
case object.scheduled_change&.action
|
65
|
+
when "cancel"
|
66
|
+
attributes[:ends_at] = Time.parse(object.scheduled_change.effective_at)
|
67
|
+
when "pause"
|
68
|
+
attributes[:pause_starts_at] = Time.parse(object.scheduled_change.effective_at)
|
69
|
+
when "resume"
|
70
|
+
attributes[:pause_resumes_at] = Time.parse(object.scheduled_change.effective_at)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Update or create the subscription
|
74
|
+
if (pay_subscription = pay_customer.subscriptions.find_by(processor_id: subscription_id))
|
75
|
+
pay_subscription.with_lock do
|
76
|
+
pay_subscription.update!(attributes)
|
77
|
+
end
|
78
|
+
pay_subscription
|
79
|
+
else
|
80
|
+
pay_customer.subscriptions.create!(attributes.merge(name: name, processor_id: subscription_id))
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def initialize(pay_subscription)
|
85
|
+
@pay_subscription = pay_subscription
|
86
|
+
end
|
87
|
+
|
88
|
+
def subscription(**options)
|
89
|
+
@paddle_billing_subscription ||= ::Paddle::Subscription.retrieve(id: processor_id, **options)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Get a transaction to update payment method
|
93
|
+
def payment_method_transaction
|
94
|
+
::Paddle::Subscription.get_transaction(id: processor_id)
|
95
|
+
end
|
96
|
+
|
97
|
+
# If a subscription is paused, cancel immediately
|
98
|
+
# Otherwise, cancel at period end
|
99
|
+
def cancel(**options)
|
100
|
+
return if canceled?
|
101
|
+
|
102
|
+
response = ::Paddle::Subscription.cancel(
|
103
|
+
id: processor_id,
|
104
|
+
effective_from: options.fetch(:effective_from, (paused? ? "immediately" : "next_billing_period"))
|
105
|
+
)
|
106
|
+
pay_subscription.update(
|
107
|
+
status: response.status,
|
108
|
+
ends_at: response.scheduled_change.effective_at
|
109
|
+
)
|
110
|
+
rescue ::Paddle::Error => e
|
111
|
+
raise Pay::PaddleBilling::Error, e
|
112
|
+
end
|
113
|
+
|
114
|
+
def cancel_now!(**options)
|
115
|
+
cancel(options.merge(effective_from: "immediately"))
|
116
|
+
rescue ::Paddle::Error => e
|
117
|
+
raise Pay::PaddleBilling::Error, e
|
118
|
+
end
|
119
|
+
|
120
|
+
def change_quantity(quantity, **options)
|
121
|
+
items = [{
|
122
|
+
price_id: processor_plan,
|
123
|
+
quantity: quantity
|
124
|
+
}]
|
125
|
+
|
126
|
+
::Paddle::Subscription.update(id: processor_id, items: items, proration_billing_mode: "prorated_immediately")
|
127
|
+
rescue ::Paddle::Error => e
|
128
|
+
raise Pay::PaddleBilling::Error, e
|
129
|
+
end
|
130
|
+
|
131
|
+
# A subscription could be set to cancel or pause in the future
|
132
|
+
# It is considered on grace period until the cancel or pause time begins
|
133
|
+
def on_grace_period?
|
134
|
+
(canceled? && Time.current < ends_at) || (paused? && pause_starts_at? && Time.current < pause_starts_at)
|
135
|
+
end
|
136
|
+
|
137
|
+
def paused?
|
138
|
+
pay_subscription.status == "paused"
|
139
|
+
end
|
140
|
+
|
141
|
+
def pause
|
142
|
+
response = ::Paddle::Subscription.pause(id: processor_id)
|
143
|
+
pay_subscription.update!(status: :paused, pause_starts_at: response.scheduled_change.effective_at)
|
144
|
+
rescue ::Paddle::Error => e
|
145
|
+
raise Pay::PaddleBilling::Error, e
|
146
|
+
end
|
147
|
+
|
148
|
+
def resumable?
|
149
|
+
paused?
|
150
|
+
end
|
151
|
+
|
152
|
+
def resume
|
153
|
+
unless resumable?
|
154
|
+
raise StandardError, "You can only resume paused subscriptions."
|
155
|
+
end
|
156
|
+
|
157
|
+
# Paddle Billing API only allows "resuming" subscriptions when they are paused
|
158
|
+
# So cancel the scheduled change if it is in the future
|
159
|
+
if paused? && pause_starts_at? && Time.current < pause_starts_at
|
160
|
+
::Paddle::Subscription.update(id: processor_id, scheduled_change: nil)
|
161
|
+
else
|
162
|
+
::Paddle::Subscription.resume(id: processor_id, effective_from: "immediately")
|
163
|
+
end
|
164
|
+
|
165
|
+
pay_subscription.update(status: :active, pause_starts_at: nil)
|
166
|
+
rescue ::Paddle::Error => e
|
167
|
+
raise Pay::PaddleBilling::Error, e
|
168
|
+
end
|
169
|
+
|
170
|
+
def swap(plan, **options)
|
171
|
+
items = [{
|
172
|
+
price_id: plan,
|
173
|
+
quantity: quantity || 1
|
174
|
+
}]
|
175
|
+
|
176
|
+
::Paddle::Subscription.update(id: processor_id, items: items, proration_billing_mode: "prorated_immediately")
|
177
|
+
pay_subscription.update(processor_plan: plan, ends_at: nil, status: :active)
|
178
|
+
end
|
179
|
+
|
180
|
+
# Retries the latest invoice for a Past Due subscription
|
181
|
+
def retry_failed_payment
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
module Pay
|
2
|
+
module LemonSqueezy
|
3
|
+
autoload :Billable, "pay/stripe/billable"
|
4
|
+
autoload :Charge, "pay/stripe/charge"
|
5
|
+
autoload :Error, "pay/stripe/error"
|
6
|
+
autoload :Merchant, "pay/stripe/merchant"
|
7
|
+
autoload :PaymentMethod, "pay/stripe/payment_method"
|
8
|
+
autoload :Subscription, "pay/stripe/subscription"
|
9
|
+
|
10
|
+
module Webhooks
|
11
|
+
autoload :AccountUpdated, "pay/stripe/webhooks/account_updated"
|
12
|
+
autoload :ChargeRefunded, "pay/stripe/webhooks/charge_refunded"
|
13
|
+
autoload :ChargeSucceeded, "pay/stripe/webhooks/charge_succeeded"
|
14
|
+
autoload :CheckoutSessionCompleted, "pay/stripe/webhooks/checkout_session_completed"
|
15
|
+
autoload :CheckoutSessionAsyncPaymentSucceeded, "pay/stripe/webhooks/checkout_session_async_payment_succeeded"
|
16
|
+
autoload :CustomerDeleted, "pay/stripe/webhooks/customer_deleted"
|
17
|
+
autoload :CustomerUpdated, "pay/stripe/webhooks/customer_updated"
|
18
|
+
autoload :PaymentActionRequired, "pay/stripe/webhooks/payment_action_required"
|
19
|
+
autoload :PaymentFailed, "pay/stripe/webhooks/payment_failed"
|
20
|
+
autoload :PaymentIntentSucceeded, "pay/stripe/webhooks/payment_intent_succeeded"
|
21
|
+
autoload :PaymentMethodAttached, "pay/stripe/webhooks/payment_method_attached"
|
22
|
+
autoload :PaymentMethodDetached, "pay/stripe/webhooks/payment_method_detached"
|
23
|
+
autoload :PaymentMethodUpdated, "pay/stripe/webhooks/payment_method_updated"
|
24
|
+
autoload :SubscriptionCreated, "pay/stripe/webhooks/subscription_created"
|
25
|
+
autoload :SubscriptionDeleted, "pay/stripe/webhooks/subscription_deleted"
|
26
|
+
autoload :SubscriptionRenewing, "pay/stripe/webhooks/subscription_renewing"
|
27
|
+
autoload :SubscriptionUpdated, "pay/stripe/webhooks/subscription_updated"
|
28
|
+
autoload :SubscriptionTrialWillEnd, "pay/stripe/webhooks/subscription_trial_will_end"
|
29
|
+
end
|
30
|
+
|
31
|
+
extend Env
|
32
|
+
|
33
|
+
REQUIRED_VERSION = "~> 1"
|
34
|
+
|
35
|
+
def self.enabled?
|
36
|
+
return false unless Pay.enabled_processors.include?(:lemonsqueezy) && defined?(::Lemonzsqueezy)
|
37
|
+
|
38
|
+
Pay::Engine.version_matches?(required: REQUIRED_VERSION, current: ::Lemonsqueezy::VERSION) || (raise "[Pay] lemonsqueezy gem must be version #{REQUIRED_VERSION}")
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.setup
|
42
|
+
::Stripe.api_key = private_key
|
43
|
+
|
44
|
+
# Used by Stripe to identify Pay for support
|
45
|
+
::Stripe.set_app_info("PayRails", partner_id: "pp_partner_IqhY0UExnJYLxg", version: Pay::VERSION, url: "https://github.com/pay-rails/pay")
|
46
|
+
|
47
|
+
# Automatically retry requests that fail
|
48
|
+
# This automatically includes idempotency keys in the request to guarantee that retires are safe
|
49
|
+
# https://github.com/stripe/stripe-ruby#configuring-automatic-retries
|
50
|
+
::Stripe.max_network_retries = 2
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.public_key
|
54
|
+
find_value_by_name(:stripe, :public_key)
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.private_key
|
58
|
+
find_value_by_name(:stripe, :private_key)
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.signing_secret
|
62
|
+
find_value_by_name(:stripe, :signing_secret)
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.configure_webhooks
|
66
|
+
Pay::Webhooks.configure do |events|
|
67
|
+
# Listen to the charge event to make sure we get non-subscription
|
68
|
+
# purchases as well. Invoice is only for subscriptions and manual creation
|
69
|
+
# so it does not include individual charges.
|
70
|
+
events.subscribe "stripe.charge.succeeded", Pay::Stripe::Webhooks::ChargeSucceeded.new
|
71
|
+
events.subscribe "stripe.charge.refunded", Pay::Stripe::Webhooks::ChargeRefunded.new
|
72
|
+
|
73
|
+
events.subscribe "stripe.payment_intent.succeeded", Pay::Stripe::Webhooks::PaymentIntentSucceeded.new
|
74
|
+
|
75
|
+
# Warn user of upcoming charges for their subscription. This is handy for
|
76
|
+
# notifying annual users their subscription will renew shortly.
|
77
|
+
# This probably should be ignored for monthly subscriptions.
|
78
|
+
events.subscribe "stripe.invoice.upcoming", Pay::Stripe::Webhooks::SubscriptionRenewing.new
|
79
|
+
|
80
|
+
# Payment action is required to process an invoice
|
81
|
+
events.subscribe "stripe.invoice.payment_action_required", Pay::Stripe::Webhooks::PaymentActionRequired.new
|
82
|
+
|
83
|
+
# If an invoice payment fails, we want to notify the user via email to update their payment details
|
84
|
+
events.subscribe "stripe.invoice.payment_failed", Pay::Stripe::Webhooks::PaymentFailed.new
|
85
|
+
|
86
|
+
# If a subscription is manually created on Stripe, we want to sync
|
87
|
+
events.subscribe "stripe.customer.subscription.created", Pay::Stripe::Webhooks::SubscriptionCreated.new
|
88
|
+
|
89
|
+
# If the plan, quantity, or trial ending date is updated on Stripe, we want to sync
|
90
|
+
events.subscribe "stripe.customer.subscription.updated", Pay::Stripe::Webhooks::SubscriptionUpdated.new
|
91
|
+
|
92
|
+
# When a customers subscription is canceled, we want to update our records
|
93
|
+
events.subscribe "stripe.customer.subscription.deleted", Pay::Stripe::Webhooks::SubscriptionDeleted.new
|
94
|
+
|
95
|
+
# When a customers subscription trial period is 3 days from ending or ended immediately this event is fired
|
96
|
+
events.subscribe "stripe.customer.subscription.trial_will_end", Pay::Stripe::Webhooks::SubscriptionTrialWillEnd.new
|
97
|
+
|
98
|
+
# Monitor changes for customer's default card changing and invoice credit updates
|
99
|
+
events.subscribe "stripe.customer.updated", Pay::Stripe::Webhooks::CustomerUpdated.new
|
100
|
+
|
101
|
+
# If a customer was deleted in Stripe, their subscriptions should be cancelled
|
102
|
+
events.subscribe "stripe.customer.deleted", Pay::Stripe::Webhooks::CustomerDeleted.new
|
103
|
+
|
104
|
+
# If a customer's payment source was deleted in Stripe, we should update as well
|
105
|
+
events.subscribe "stripe.payment_method.attached", Pay::Stripe::Webhooks::PaymentMethodAttached.new
|
106
|
+
events.subscribe "stripe.payment_method.updated", Pay::Stripe::Webhooks::PaymentMethodUpdated.new
|
107
|
+
events.subscribe "stripe.payment_method.card_automatically_updated", Pay::Stripe::Webhooks::PaymentMethodUpdated.new
|
108
|
+
events.subscribe "stripe.payment_method.detached", Pay::Stripe::Webhooks::PaymentMethodDetached.new
|
109
|
+
|
110
|
+
# If an account is updated in stripe, we should update it as well
|
111
|
+
events.subscribe "stripe.account.updated", Pay::Stripe::Webhooks::AccountUpdated.new
|
112
|
+
|
113
|
+
# Handle subscriptions in Stripe Checkout Sessions
|
114
|
+
events.subscribe "stripe.checkout.session.completed", Pay::Stripe::Webhooks::CheckoutSessionCompleted.new
|
115
|
+
events.subscribe "stripe.checkout.session.async_payment_succeeded", Pay::Stripe::Webhooks::CheckoutSessionAsyncPaymentSucceeded.new
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.to_client_reference_id(record)
|
120
|
+
raise ArgumentError, "#{record.class.name} does not include Pay. Allowed models: #{model_names.to_a.join(", ")}" unless model_names.include?(record.class.name)
|
121
|
+
[record.class.name, record.id].join("_")
|
122
|
+
end
|
123
|
+
|
124
|
+
def self.find_by_client_reference_id(client_reference_id)
|
125
|
+
# If there is a client reference ID, make sure we have a Pay::Customer record
|
126
|
+
# client_reference_id should be in the format of "User/1"
|
127
|
+
model_name, id = client_reference_id.split("_", 2)
|
128
|
+
|
129
|
+
# Only allow model names that use Pay
|
130
|
+
return unless model_names.include?(model_name)
|
131
|
+
|
132
|
+
model_name.constantize.find(id)
|
133
|
+
rescue ActiveRecord::RecordNotFound
|
134
|
+
Rails.logger.error "[Pay] Unable to locate record with: #{client_reference_id}"
|
135
|
+
nil
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -37,8 +37,8 @@ module Pay
|
|
37
37
|
subscription: pay_customer.subscriptions.find_by(processor_id: object.subscription_id)
|
38
38
|
}
|
39
39
|
|
40
|
-
if object.
|
41
|
-
case
|
40
|
+
if (details = Array.wrap(object.payments).first&.method_details)
|
41
|
+
case details.type.downcase
|
42
42
|
when "card"
|
43
43
|
attrs[:payment_method_type] = "card"
|
44
44
|
attrs[:brand] = details.card.type
|
@@ -5,6 +5,13 @@ module Pay
|
|
5
5
|
|
6
6
|
delegate :customer, :processor_id, to: :pay_payment_method
|
7
7
|
|
8
|
+
def self.sync_from_transaction(pay_customer:, transaction:)
|
9
|
+
transaction = ::Paddle::Transaction.retrieve(id: transaction)
|
10
|
+
return unless transaction.status == "completed"
|
11
|
+
return if transaction.payments.empty?
|
12
|
+
sync(pay_customer: pay_customer, attributes: transaction.payments.first)
|
13
|
+
end
|
14
|
+
|
8
15
|
def self.sync(pay_customer:, attributes:)
|
9
16
|
details = attributes.method_details
|
10
17
|
attrs = {
|
@@ -19,7 +26,7 @@ module Pay
|
|
19
26
|
attrs[:exp_year] = details.card.expiry_year
|
20
27
|
end
|
21
28
|
|
22
|
-
payment_method = pay_customer.payment_methods.find_or_initialize_by(processor_id: attributes.
|
29
|
+
payment_method = pay_customer.payment_methods.find_or_initialize_by(processor_id: attributes.payment_method_id)
|
23
30
|
payment_method.update!(attrs)
|
24
31
|
payment_method
|
25
32
|
end
|
@@ -56,9 +56,9 @@ module Pay
|
|
56
56
|
# Remove payment methods since customer cannot be reused after cancelling
|
57
57
|
Pay::PaymentMethod.where(customer_id: object.customer_id).destroy_all
|
58
58
|
when "trialing"
|
59
|
-
attributes[:trial_ends_at] = Time.parse(object.next_billed_at)
|
59
|
+
attributes[:trial_ends_at] = Time.parse(object.next_billed_at) if object.next_billed_at
|
60
60
|
when "paused"
|
61
|
-
attributes[:pause_starts_at] = Time.parse(object.paused_at)
|
61
|
+
attributes[:pause_starts_at] = Time.parse(object.paused_at) if object.paused_at
|
62
62
|
when "active", "past_due"
|
63
63
|
attributes[:trial_ends_at] = nil
|
64
64
|
attributes[:pause_starts_at] = nil
|
@@ -109,7 +109,7 @@ module Pay
|
|
109
109
|
)
|
110
110
|
pay_subscription.update(
|
111
111
|
status: response.status,
|
112
|
-
ends_at: response.scheduled_change.
|
112
|
+
ends_at: response.scheduled_change&.effective_at || Time.current
|
113
113
|
)
|
114
114
|
rescue ::Paddle::Error => e
|
115
115
|
raise Pay::PaddleBilling::Error, e
|
@@ -207,7 +207,7 @@ module Pay
|
|
207
207
|
# cancel_now!(prorate: true)
|
208
208
|
# cancel_now!(invoice_now: true)
|
209
209
|
def cancel_now!(**options)
|
210
|
-
return if canceled?
|
210
|
+
return if canceled? && ends_at.past?
|
211
211
|
|
212
212
|
@stripe_subscription = ::Stripe::Subscription.cancel(processor_id, options.merge(expand_options), stripe_options)
|
213
213
|
pay_subscription.update(ends_at: Time.current, status: :canceled)
|
@@ -261,6 +261,8 @@ module Pay
|
|
261
261
|
#
|
262
262
|
# 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`
|
263
263
|
# Other pause_behaviors do not set `pause_starts_at` because they are used for offering free services
|
264
|
+
#
|
265
|
+
# https://docs.stripe.com/billing/subscriptions/pause-payment
|
264
266
|
def pause(**options)
|
265
267
|
attributes = {pause_collection: options.reverse_merge(behavior: "void")}
|
266
268
|
@stripe_subscription = ::Stripe::Subscription.update(processor_id, attributes.merge(expand_options), stripe_options)
|
@@ -273,8 +275,10 @@ module Pay
|
|
273
275
|
end
|
274
276
|
|
275
277
|
# Unpauses a subscription
|
278
|
+
#
|
279
|
+
# https://docs.stripe.com/billing/subscriptions/pause-payment#unpausing
|
276
280
|
def unpause
|
277
|
-
@stripe_subscription = ::Stripe::Subscription.update(processor_id, {pause_collection:
|
281
|
+
@stripe_subscription = ::Stripe::Subscription.update(processor_id, {pause_collection: ""}.merge(expand_options), stripe_options)
|
278
282
|
pay_subscription.update(
|
279
283
|
pause_behavior: nil,
|
280
284
|
pause_resumes_at: nil,
|
data/lib/pay/stripe.rb
CHANGED
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: 7.
|
4
|
+
version: 7.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jason Charnes
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2024-
|
13
|
+
date: 2024-05-29 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: rails
|
@@ -104,6 +104,14 @@ files:
|
|
104
104
|
- lib/pay/fake_processor/merchant.rb
|
105
105
|
- lib/pay/fake_processor/payment_method.rb
|
106
106
|
- lib/pay/fake_processor/subscription.rb
|
107
|
+
- lib/pay/lemon_squeezy.rb
|
108
|
+
- lib/pay/lemon_squeezy/billable.rb
|
109
|
+
- lib/pay/lemon_squeezy/charge.rb
|
110
|
+
- lib/pay/lemon_squeezy/error.rb
|
111
|
+
- lib/pay/lemon_squeezy/payment_method.rb
|
112
|
+
- lib/pay/lemon_squeezy/subscription.rb
|
113
|
+
- lib/pay/lemon_squeezy/webhooks/subscription.rb
|
114
|
+
- lib/pay/lemon_squeezy/webhooks/transaction_completed.rb
|
107
115
|
- lib/pay/nano_id.rb
|
108
116
|
- lib/pay/paddle_billing.rb
|
109
117
|
- lib/pay/paddle_billing/billable.rb
|
@@ -176,7 +184,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
176
184
|
- !ruby/object:Gem::Version
|
177
185
|
version: '0'
|
178
186
|
requirements: []
|
179
|
-
rubygems_version: 3.
|
187
|
+
rubygems_version: 3.4.22
|
180
188
|
signing_key:
|
181
189
|
specification_version: 4
|
182
190
|
summary: Payments engine for Ruby on Rails
|