pay 2.3.1 → 2.5.0
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/README.md +7 -5
- data/app/controllers/pay/webhooks/braintree_controller.rb +7 -53
- data/app/controllers/pay/webhooks/paddle_controller.rb +19 -18
- data/app/controllers/pay/webhooks/stripe_controller.rb +47 -0
- data/app/mailers/pay/user_mailer.rb +14 -35
- data/app/models/pay/subscription.rb +12 -18
- data/app/views/pay/user_mailer/payment_action_required.html.erb +1 -1
- data/app/views/pay/user_mailer/receipt.html.erb +6 -6
- data/app/views/pay/user_mailer/refund.html.erb +6 -6
- data/app/views/pay/user_mailer/subscription_renewing.html.erb +1 -1
- data/config/locales/en.yml +137 -0
- data/config/routes.rb +1 -1
- data/db/migrate/20200603134434_add_data_to_pay_models.rb +6 -1
- data/lib/pay.rb +19 -50
- data/lib/pay/billable.rb +9 -9
- data/lib/pay/braintree.rb +1 -0
- data/lib/pay/braintree/billable.rb +11 -11
- data/lib/pay/braintree/charge.rb +3 -3
- data/lib/pay/braintree/subscription.rb +21 -13
- data/lib/pay/braintree/webhooks.rb +11 -0
- data/lib/pay/braintree/webhooks/subscription_canceled.rb +19 -0
- data/lib/pay/braintree/webhooks/subscription_charged_successfully.rb +24 -0
- data/lib/pay/braintree/webhooks/subscription_charged_unsuccessfully.rb +24 -0
- data/lib/pay/braintree/webhooks/subscription_expired.rb +19 -0
- data/lib/pay/braintree/webhooks/subscription_trial_ended.rb +19 -0
- data/lib/pay/braintree/webhooks/subscription_went_active.rb +19 -0
- data/lib/pay/braintree/webhooks/subscription_went_past_due.rb +19 -0
- data/lib/pay/engine.rb +0 -1
- data/lib/pay/errors.rb +73 -0
- data/lib/pay/paddle/billable.rb +34 -5
- data/lib/pay/paddle/charge.rb +2 -2
- data/lib/pay/paddle/subscription.rb +22 -13
- data/lib/pay/paddle/webhooks.rb +8 -0
- data/lib/pay/paddle/webhooks/subscription_cancelled.rb +3 -3
- data/lib/pay/paddle/webhooks/subscription_created.rb +15 -15
- data/lib/pay/paddle/webhooks/subscription_payment_refunded.rb +5 -5
- data/lib/pay/paddle/webhooks/subscription_payment_succeeded.rb +17 -38
- data/lib/pay/paddle/webhooks/subscription_updated.rb +20 -17
- data/lib/pay/receipts.rb +6 -6
- data/lib/pay/stripe.rb +3 -1
- data/lib/pay/stripe/billable.rb +4 -4
- data/lib/pay/stripe/charge.rb +2 -2
- data/lib/pay/stripe/subscription.rb +20 -12
- data/lib/pay/stripe/webhooks.rb +14 -15
- data/lib/pay/stripe/webhooks/charge_refunded.rb +2 -2
- data/lib/pay/stripe/webhooks/charge_succeeded.rb +1 -1
- data/lib/pay/stripe/webhooks/payment_action_required.rb +1 -1
- data/lib/pay/stripe/webhooks/subscription_created.rb +2 -1
- data/lib/pay/stripe/webhooks/subscription_renewing.rb +4 -3
- data/lib/pay/version.rb +1 -1
- data/lib/pay/webhooks/delegator.rb +61 -0
- metadata +21 -88
@@ -0,0 +1,19 @@
|
|
1
|
+
# A subscription reaches the specified number of billing cycles and expires.
|
2
|
+
|
3
|
+
module Pay
|
4
|
+
module Braintree
|
5
|
+
module Webhooks
|
6
|
+
class SubscriptionExpired
|
7
|
+
def call(event)
|
8
|
+
subscription = event.subscription
|
9
|
+
return if subscription.nil?
|
10
|
+
|
11
|
+
pay_subscription = Pay.subscription_model.find_by(processor: :braintree, processor_id: subscription.id)
|
12
|
+
return unless pay_subscription.present?
|
13
|
+
|
14
|
+
pay_subscription.update!(ends_at: Time.current, status: :canceled)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# A subscription's trial period ends.
|
2
|
+
|
3
|
+
module Pay
|
4
|
+
module Braintree
|
5
|
+
module Webhooks
|
6
|
+
class SubscriptionTrialEnded
|
7
|
+
def call(event)
|
8
|
+
subscription = event.subscription
|
9
|
+
return if subscription.nil?
|
10
|
+
|
11
|
+
pay_subscription = Pay.subscription_model.find_by(processor: :braintree, processor_id: subscription.id)
|
12
|
+
return unless pay_subscription.present?
|
13
|
+
|
14
|
+
pay_subscription.update(trial_ends_at: Time.zone.now)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# A subscription's first authorized transaction is created, or a successful transaction moves a subscription from the Past Due status to the Active status. Subscriptions with trial periods will not trigger this notification when they move from the trial period into the first billing cycle.
|
2
|
+
|
3
|
+
module Pay
|
4
|
+
module Braintree
|
5
|
+
module Webhooks
|
6
|
+
class SubscriptionWentActive
|
7
|
+
def call(event)
|
8
|
+
subscription = event.subscription
|
9
|
+
return if subscription.nil?
|
10
|
+
|
11
|
+
pay_subscription = Pay.subscription_model.find_by(processor: :braintree, processor_id: subscription.id)
|
12
|
+
return unless pay_subscription.present?
|
13
|
+
|
14
|
+
pay_subscription.update!(status: :active)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# A subscription has moved from the Active status to the Past Due status. This will only be triggered when the initial transaction in a billing cycle is declined. Once the status moves to past due, it will not be triggered again in that billing cycle.
|
2
|
+
|
3
|
+
module Pay
|
4
|
+
module Braintree
|
5
|
+
module Webhooks
|
6
|
+
class SubscriptionWentPastDue
|
7
|
+
def call(event)
|
8
|
+
subscription = event.subscription
|
9
|
+
return if subscription.nil?
|
10
|
+
|
11
|
+
pay_subscription = Pay.subscription_model.find_by(processor: :braintree, processor_id: subscription.id)
|
12
|
+
return unless pay_subscription.present?
|
13
|
+
|
14
|
+
pay_subscription.update!(status: :past_due)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/pay/engine.rb
CHANGED
data/lib/pay/errors.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
module Pay
|
2
|
+
class Error < StandardError
|
3
|
+
attr_reader :result
|
4
|
+
|
5
|
+
def initialize(result = nil)
|
6
|
+
@result = result
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class PaymentError < StandardError
|
11
|
+
attr_reader :payment
|
12
|
+
|
13
|
+
def initialize(payment)
|
14
|
+
@payment = payment
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class ActionRequired < PaymentError
|
19
|
+
def message
|
20
|
+
I18n.t("errors.action_required")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class InvalidPaymentMethod < PaymentError
|
25
|
+
def message
|
26
|
+
I18n.t("errors.invalid_payment")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
module Braintree
|
31
|
+
class Error < Error
|
32
|
+
def message
|
33
|
+
result.message
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class AuthorizationError < Braintree::Error
|
38
|
+
def message
|
39
|
+
I18n.t("errors.braintree.authorization")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
module Stripe
|
45
|
+
class Error < Error
|
46
|
+
def message
|
47
|
+
I18n.t("errors.stripe.#{result.code}", default: result.message)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
module Paddle
|
53
|
+
class Error < Error
|
54
|
+
def message
|
55
|
+
I18n.t("errors.paddle.#{result.code}", default: result.message)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class BraintreeError < Braintree::Error
|
61
|
+
def message
|
62
|
+
ActiveSupport::Deprecation.warn("Pay::BraintreeError is deprecated. Instead, use `Pay::Braintree::Error`.")
|
63
|
+
super
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
class BraintreeAuthorizationError < BraintreeError
|
68
|
+
def message
|
69
|
+
ActiveSupport::Deprecation.warn("Pay::BraintreeAuthorizationError is deprecated. Instead, use `Pay::Braintree::AuthorizationError`.")
|
70
|
+
I18n.t("errors.braintree.authorization")
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/lib/pay/paddle/billable.rb
CHANGED
@@ -23,11 +23,11 @@ module Pay
|
|
23
23
|
amount: Integer(response[:amount].to_f * 100),
|
24
24
|
card_type: subscription.processor_subscription.payment_information[:payment_method],
|
25
25
|
paddle_receipt_url: response[:receipt_url],
|
26
|
-
created_at:
|
26
|
+
created_at: Time.zone.parse(response[:payment_date])
|
27
27
|
)
|
28
28
|
charge
|
29
29
|
rescue ::PaddlePay::PaddlePayError => e
|
30
|
-
raise Error, e
|
30
|
+
raise Pay::Paddle::Error, e
|
31
31
|
end
|
32
32
|
|
33
33
|
def create_paddle_subscription(name, plan, options = {})
|
@@ -35,7 +35,7 @@ module Pay
|
|
35
35
|
end
|
36
36
|
|
37
37
|
def update_paddle_card(token)
|
38
|
-
|
38
|
+
sync_payment_information_from_paddle
|
39
39
|
end
|
40
40
|
|
41
41
|
def update_paddle_email!
|
@@ -44,14 +44,14 @@ module Pay
|
|
44
44
|
|
45
45
|
def paddle_trial_end_date(subscription)
|
46
46
|
return unless subscription.state == "trialing"
|
47
|
-
|
47
|
+
Time.zone.parse(subscription.next_payment[:date]).end_of_day
|
48
48
|
end
|
49
49
|
|
50
50
|
def paddle_subscription(subscription_id, options = {})
|
51
51
|
hash = PaddlePay::Subscription::User.list({subscription_id: subscription_id}, options).try(:first)
|
52
52
|
OpenStruct.new(hash)
|
53
53
|
rescue ::PaddlePay::PaddlePayError => e
|
54
|
-
raise Error, e
|
54
|
+
raise Pay::Paddle::Error, e
|
55
55
|
end
|
56
56
|
|
57
57
|
def paddle_invoice!(options = {})
|
@@ -61,6 +61,35 @@ module Pay
|
|
61
61
|
def paddle_upcoming_invoice
|
62
62
|
# pass
|
63
63
|
end
|
64
|
+
|
65
|
+
def sync_payment_information_from_paddle
|
66
|
+
payment_information = paddle_payment_information(subscription.processor_id)
|
67
|
+
update!(payment_information) unless payment_information.empty?
|
68
|
+
rescue ::PaddlePay::PaddlePayError => e
|
69
|
+
raise Pay::Paddle::Error, e
|
70
|
+
end
|
71
|
+
|
72
|
+
def paddle_payment_information(subscription_id)
|
73
|
+
subscription_user = PaddlePay::Subscription::User.list({subscription_id: subscription_id}).try(:first)
|
74
|
+
payment_information = subscription_user ? subscription_user[:payment_information] : nil
|
75
|
+
return {} if payment_information.nil?
|
76
|
+
|
77
|
+
case payment_information[:payment_method]
|
78
|
+
when "card"
|
79
|
+
{
|
80
|
+
card_type: payment_information[:card_type],
|
81
|
+
card_last4: payment_information[:last_four_digits],
|
82
|
+
card_exp_month: payment_information[:expiry_date].split("/").first,
|
83
|
+
card_exp_year: payment_information[:expiry_date].split("/").last
|
84
|
+
}
|
85
|
+
when "paypal"
|
86
|
+
{
|
87
|
+
card_type: "PayPal"
|
88
|
+
}
|
89
|
+
else
|
90
|
+
{}
|
91
|
+
end
|
92
|
+
end
|
64
93
|
end
|
65
94
|
end
|
66
95
|
end
|
data/lib/pay/paddle/charge.rb
CHANGED
@@ -19,7 +19,7 @@ module Pay
|
|
19
19
|
charges = payments.select { |p| p[:id].to_s == processor_id }
|
20
20
|
charges.try(:first)
|
21
21
|
rescue ::PaddlePay::PaddlePayError => e
|
22
|
-
raise Error, e
|
22
|
+
raise Pay::Paddle::Error, e
|
23
23
|
end
|
24
24
|
|
25
25
|
def paddle_refund!(amount_to_refund)
|
@@ -32,7 +32,7 @@ module Pay
|
|
32
32
|
raise Error, "Payment not found"
|
33
33
|
end
|
34
34
|
rescue ::PaddlePay::PaddlePayError => e
|
35
|
-
raise Error, e
|
35
|
+
raise Pay::Paddle::Error, e
|
36
36
|
end
|
37
37
|
end
|
38
38
|
end
|
@@ -4,14 +4,9 @@ module Pay
|
|
4
4
|
extend ActiveSupport::Concern
|
5
5
|
|
6
6
|
included do
|
7
|
-
scope :paddle, -> { where(processor: :paddle) }
|
8
|
-
|
9
7
|
store_accessor :data, :paddle_update_url
|
10
8
|
store_accessor :data, :paddle_cancel_url
|
11
|
-
|
12
|
-
|
13
|
-
def paddle?
|
14
|
-
processor == "paddle"
|
9
|
+
store_accessor :data, :paddle_paused_from
|
15
10
|
end
|
16
11
|
|
17
12
|
def paddle_cancel
|
@@ -20,31 +15,45 @@ module Pay
|
|
20
15
|
if on_trial?
|
21
16
|
update(status: :canceled, ends_at: trial_ends_at)
|
22
17
|
else
|
23
|
-
update(status: :canceled, ends_at:
|
18
|
+
update(status: :canceled, ends_at: Time.zone.parse(subscription.next_payment[:date]))
|
24
19
|
end
|
25
20
|
rescue ::PaddlePay::PaddlePayError => e
|
26
|
-
raise Error, e
|
21
|
+
raise Pay::Paddle::Error, e
|
27
22
|
end
|
28
23
|
|
29
24
|
def paddle_cancel_now!
|
30
25
|
PaddlePay::Subscription::User.cancel(processor_id)
|
31
26
|
update(status: :canceled, ends_at: Time.zone.now)
|
32
27
|
rescue ::PaddlePay::PaddlePayError => e
|
33
|
-
raise Error, e
|
28
|
+
raise Pay::Paddle::Error, e
|
29
|
+
end
|
30
|
+
|
31
|
+
def paddle_on_grace_period?
|
32
|
+
canceled? && Time.zone.now < ends_at || paused? && Time.zone.now < paddle_paused_from
|
33
|
+
end
|
34
|
+
|
35
|
+
def paddle_paused?
|
36
|
+
paddle_paused_from.present?
|
34
37
|
end
|
35
38
|
|
36
39
|
def paddle_pause
|
37
40
|
attributes = {pause: true}
|
38
41
|
response = PaddlePay::Subscription::User.update(processor_id, attributes)
|
39
|
-
update(
|
42
|
+
update(paddle_paused_from: Time.zone.parse(response[:next_payment][:date]))
|
43
|
+
rescue ::PaddlePay::PaddlePayError => e
|
44
|
+
raise Pay::Paddle::Error, e
|
40
45
|
end
|
41
46
|
|
42
47
|
def paddle_resume
|
48
|
+
unless paused?
|
49
|
+
raise StandardError, "You can only resume paused subscriptions."
|
50
|
+
end
|
51
|
+
|
43
52
|
attributes = {pause: false}
|
44
53
|
PaddlePay::Subscription::User.update(processor_id, attributes)
|
45
|
-
update(status: :active,
|
54
|
+
update(status: :active, paddle_paused_from: nil)
|
46
55
|
rescue ::PaddlePay::PaddlePayError => e
|
47
|
-
raise Error, e
|
56
|
+
raise Pay::Paddle::Error, e
|
48
57
|
end
|
49
58
|
|
50
59
|
def paddle_swap(plan)
|
@@ -52,7 +61,7 @@ module Pay
|
|
52
61
|
attributes[:quantity] = quantity if quantity?
|
53
62
|
PaddlePay::Subscription::User.update(processor_id, attributes)
|
54
63
|
rescue ::PaddlePay::PaddlePayError => e
|
55
|
-
raise Error, e
|
64
|
+
raise Pay::Paddle::Error, e
|
56
65
|
end
|
57
66
|
end
|
58
67
|
end
|
data/lib/pay/paddle/webhooks.rb
CHANGED
@@ -1 +1,9 @@
|
|
1
1
|
Dir[File.join(__dir__, "webhooks", "**", "*.rb")].sort.each { |file| require file }
|
2
|
+
|
3
|
+
Pay::Webhooks.configure do |events|
|
4
|
+
events.subscribe "paddle.subscription_created", Pay::Paddle::Webhooks::SubscriptionCreated.new
|
5
|
+
events.subscribe "paddle.subscription_updated", Pay::Paddle::Webhooks::SubscriptionUpdated.new
|
6
|
+
events.subscribe "paddle.subscription_cancelled", Pay::Paddle::Webhooks::SubscriptionCancelled.new
|
7
|
+
events.subscribe "paddle.subscription_payment_succeeded", Pay::Paddle::Webhooks::SubscriptionPaymentSucceeded.new
|
8
|
+
events.subscribe "paddle.subscription_payment_refunded", Pay::Paddle::Webhooks::SubscriptionPaymentRefunded.new
|
9
|
+
end
|
@@ -2,15 +2,15 @@ module Pay
|
|
2
2
|
module Paddle
|
3
3
|
module Webhooks
|
4
4
|
class SubscriptionCancelled
|
5
|
-
def
|
6
|
-
subscription = Pay.subscription_model.find_by(processor: :paddle, processor_id:
|
5
|
+
def call(event)
|
6
|
+
subscription = Pay.subscription_model.find_by(processor: :paddle, processor_id: event["subscription_id"])
|
7
7
|
|
8
8
|
# We couldn't find the subscription for some reason, maybe it's from another service
|
9
9
|
return if subscription.nil?
|
10
10
|
|
11
11
|
# User canceled subscriptions have an ends_at
|
12
12
|
# Automatically canceled subscriptions need this value set
|
13
|
-
subscription.update!(ends_at:
|
13
|
+
subscription.update!(ends_at: Time.zone.parse(event["cancellation_effective_date"])) if subscription.ends_at.blank? && event["cancellation_effective_date"].present?
|
14
14
|
end
|
15
15
|
end
|
16
16
|
end
|
@@ -2,42 +2,42 @@ module Pay
|
|
2
2
|
module Paddle
|
3
3
|
module Webhooks
|
4
4
|
class SubscriptionCreated
|
5
|
-
def
|
5
|
+
def call(event)
|
6
6
|
# We may already have the subscription in the database, so we can update that record
|
7
|
-
subscription = Pay.subscription_model.find_by(processor: :paddle, processor_id:
|
7
|
+
subscription = Pay.subscription_model.find_by(processor: :paddle, processor_id: event["subscription_id"])
|
8
8
|
|
9
9
|
# Create the subscription in the database if we don't have it already
|
10
10
|
if subscription.nil?
|
11
11
|
|
12
12
|
# The customer could already be in the database
|
13
|
-
owner = Pay.find_billable(processor: :paddle, processor_id:
|
13
|
+
owner = Pay.find_billable(processor: :paddle, processor_id: event["user_id"])
|
14
14
|
|
15
15
|
if owner.nil?
|
16
|
-
owner = owner_by_passtrough(
|
17
|
-
owner&.update!(processor: "paddle", processor_id:
|
16
|
+
owner = owner_by_passtrough(event["passthrough"], event["subscription_plan_id"])
|
17
|
+
owner&.update!(processor: "paddle", processor_id: event["user_id"])
|
18
18
|
end
|
19
19
|
|
20
20
|
if owner.nil?
|
21
|
-
Rails.logger.error("[Pay] Unable to find Pay::Billable with owner: '#{
|
21
|
+
Rails.logger.error("[Pay] Unable to find Pay::Billable with owner: '#{event["passthrough"]}'. Searched these models: #{Pay.billable_models.join(", ")}")
|
22
22
|
return
|
23
23
|
end
|
24
24
|
|
25
|
-
subscription = Pay.subscription_model.new(owner: owner, name:
|
25
|
+
subscription = Pay.subscription_model.new(owner: owner, name: Pay.default_product_name, processor: "paddle", processor_id: event["subscription_id"], status: :active)
|
26
26
|
end
|
27
27
|
|
28
|
-
subscription.quantity =
|
29
|
-
subscription.processor_plan =
|
30
|
-
subscription.paddle_update_url =
|
31
|
-
subscription.paddle_cancel_url =
|
32
|
-
subscription.trial_ends_at = Time.zone.parse(
|
28
|
+
subscription.quantity = event["quantity"]
|
29
|
+
subscription.processor_plan = event["subscription_plan_id"]
|
30
|
+
subscription.paddle_update_url = event["update_url"]
|
31
|
+
subscription.paddle_cancel_url = event["cancel_url"]
|
32
|
+
subscription.trial_ends_at = Time.zone.parse(event["next_bill_date"]) if event["status"] == "trialing"
|
33
33
|
|
34
34
|
# If user was on trial, their subscription ends at the end of the trial
|
35
|
-
subscription.ends_at = if ["paused", "deleted"].include?(
|
35
|
+
subscription.ends_at = if ["paused", "deleted"].include?(event["status"]) && subscription.on_trial?
|
36
36
|
subscription.trial_ends_at
|
37
37
|
|
38
38
|
# User wasn't on trial, so subscription ends at period end
|
39
|
-
elsif ["paused", "deleted"].include?(
|
40
|
-
Time.zone.parse(
|
39
|
+
elsif ["paused", "deleted"].include?(event["status"])
|
40
|
+
Time.zone.parse(event["next_bill_date"])
|
41
41
|
|
42
42
|
# Subscription isn't marked to cancel at period end
|
43
43
|
end
|
@@ -2,17 +2,17 @@ module Pay
|
|
2
2
|
module Paddle
|
3
3
|
module Webhooks
|
4
4
|
class SubscriptionPaymentRefunded
|
5
|
-
def
|
6
|
-
charge = Pay.charge_model.find_by(processor: :paddle, processor_id:
|
5
|
+
def call(event)
|
6
|
+
charge = Pay.charge_model.find_by(processor: :paddle, processor_id: event["subscription_payment_id"])
|
7
7
|
return unless charge.present?
|
8
8
|
|
9
|
-
charge.update(amount_refunded: Integer(
|
9
|
+
charge.update(amount_refunded: Integer(event["gross_refund"].to_f * 100))
|
10
10
|
notify_user(charge.owner, charge)
|
11
11
|
end
|
12
12
|
|
13
|
-
def notify_user(
|
13
|
+
def notify_user(billable, charge)
|
14
14
|
if Pay.send_emails
|
15
|
-
Pay::UserMailer.
|
15
|
+
Pay::UserMailer.with(billable: billable, charge: charge).refund.deliver_later
|
16
16
|
end
|
17
17
|
end
|
18
18
|
end
|