pay 11.8.0 → 12.0.0
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/controllers/pay/payments_controller.rb +3 -11
- data/app/controllers/pay/webhooks/base_controller.rb +50 -0
- data/app/controllers/pay/webhooks/braintree_controller.rb +11 -18
- data/app/controllers/pay/webhooks/lemon_squeezy_controller.rb +8 -26
- data/app/controllers/pay/webhooks/paddle_billing_controller.rb +13 -35
- data/app/controllers/pay/webhooks/paddle_classic_controller.rb +5 -20
- data/app/controllers/pay/webhooks/stripe_controller.rb +13 -29
- data/app/models/pay/braintree/charge.rb +6 -12
- data/app/models/pay/braintree/payment_method.rb +7 -5
- data/app/models/pay/braintree/subscription.rb +39 -44
- data/app/models/pay/charge.rb +1 -1
- data/app/models/pay/customer.rb +1 -9
- data/app/models/pay/lemon_squeezy/charge.rb +63 -61
- data/app/models/pay/lemon_squeezy/customer.rb +1 -1
- data/app/models/pay/lemon_squeezy/subscription.rb +43 -46
- data/app/models/pay/merchant.rb +1 -1
- data/app/models/pay/paddle_billing/charge.rb +43 -53
- data/app/models/pay/paddle_billing/customer.rb +1 -1
- data/app/models/pay/paddle_billing/payment_method.rb +19 -23
- data/app/models/pay/paddle_billing/subscription.rb +56 -67
- data/app/models/pay/paddle_classic/customer.rb +1 -1
- data/app/models/pay/paddle_classic/subscription.rb +42 -44
- data/app/models/pay/payment_method.rb +1 -5
- data/app/models/pay/stripe/charge.rb +49 -61
- data/app/models/pay/stripe/customer.rb +41 -19
- data/app/models/pay/stripe/payment_method.rb +25 -30
- data/app/models/pay/stripe/subscription.rb +103 -107
- data/app/models/pay/subscription.rb +2 -12
- data/app/views/layouts/pay/application.html.erb +0 -2
- data/app/views/pay/payments/show.html.erb +19 -8
- data/lib/pay/braintree/webhooks/subscription.rb +15 -0
- data/lib/pay/braintree/webhooks/subscription_canceled.rb +1 -7
- data/lib/pay/braintree/webhooks/subscription_expired.rb +1 -7
- data/lib/pay/braintree/webhooks/subscription_trial_ended.rb +1 -7
- data/lib/pay/braintree/webhooks/subscription_went_active.rb +2 -8
- data/lib/pay/braintree/webhooks/subscription_went_past_due.rb +2 -8
- data/lib/pay/braintree.rb +1 -2
- data/lib/pay/currency.rb +0 -4
- data/lib/pay/engine.rb +4 -0
- data/lib/pay/errors.rb +4 -0
- data/lib/pay/lemon_squeezy.rb +4 -7
- data/lib/pay/paddle_billing.rb +4 -1
- data/lib/pay/paddle_classic.rb +4 -1
- data/lib/pay/payment.rb +2 -12
- data/lib/pay/receipts.rb +0 -1
- data/lib/pay/stripe/webhooks/subscription_created.rb +2 -4
- data/lib/pay/stripe/webhooks/subscription_deleted.rb +2 -5
- data/lib/pay/stripe.rb +2 -2
- data/lib/pay/sync.rb +29 -0
- data/lib/pay/version.rb +1 -1
- data/lib/pay.rb +7 -5
- metadata +5 -3
- data/lib/pay/braintree/webhooks/subscription_charged_unsuccessfully.rb +0 -24
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 14afa87f45020e488adeabd8512769dfb6505e4a4416fcd785ddfb44b5f1f1aa
|
|
4
|
+
data.tar.gz: 24ac5cb9a899928283198d52ac59f978ba36f5ecbb42b2188605ad4ac47e3fc3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1485f2c107bb18da658f958d20d6c849b552c487f9479649296c20a46c67fcc99d3e51ea2c28c6c3edf79585a99a6723da3e613f626f69111c50609884a2a549
|
|
7
|
+
data.tar.gz: 2dffe43858ed52b7f63a1249e0dbcfc88911a9e08cfdbf17702381bf75dd4e956b77ae66cdbbcd7d50832a136a108d33dc1daafe42177219c40d475d2842cd4f
|
|
@@ -2,21 +2,13 @@ module Pay
|
|
|
2
2
|
class PaymentsController < ApplicationController
|
|
3
3
|
layout "pay/application"
|
|
4
4
|
|
|
5
|
-
before_action :set_redirect_to
|
|
6
|
-
|
|
7
5
|
# Intents on a Stripe Connect account are linked with ?stripe_account=acct_123
|
|
6
|
+
# The back link only follows a same-host URL or a path, so it can't be used for an open redirect
|
|
8
7
|
def show
|
|
9
8
|
@payment = Payment.from_id(params[:id], stripe_account: params[:stripe_account].presence)
|
|
10
|
-
|
|
9
|
+
@redirect_to = url_from(params[:back]) || root_path
|
|
10
|
+
rescue Pay::Error => e
|
|
11
11
|
redirect_to root_path, alert: e.message
|
|
12
12
|
end
|
|
13
|
-
|
|
14
|
-
private
|
|
15
|
-
|
|
16
|
-
# Ensure the back parameter is a valid path
|
|
17
|
-
# This safely handles XSS or external redirects
|
|
18
|
-
def set_redirect_to
|
|
19
|
-
@redirect_to = URI.parse(params[:back].to_s).path.presence || root_path
|
|
20
|
-
end
|
|
21
13
|
end
|
|
22
14
|
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module Pay
|
|
2
|
+
module Webhooks
|
|
3
|
+
# Verifies a processor's webhook request and queues the event for Pay::Webhooks::ProcessJob.
|
|
4
|
+
#
|
|
5
|
+
# Subclasses implement verified_event, which raises a Pay::Error when the signature doesn't
|
|
6
|
+
# check out, and event_type. Processors whose parsed event can't be serialized override
|
|
7
|
+
# event_payload; Stripe overrides queue? to skip test-mode events.
|
|
8
|
+
class BaseController < ActionController::API
|
|
9
|
+
rescue_from Pay::Error, with: :invalid_signature
|
|
10
|
+
|
|
11
|
+
def create
|
|
12
|
+
event = verified_event
|
|
13
|
+
queue_event(event) if queue?(event)
|
|
14
|
+
head :ok
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def queue?(event)
|
|
20
|
+
true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def event_payload(event)
|
|
24
|
+
event
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def queue_event(event)
|
|
28
|
+
type = event_type(event)
|
|
29
|
+
return unless Pay::Webhooks.delegator.listening?("#{processor}.#{type}")
|
|
30
|
+
|
|
31
|
+
record = Pay::Webhook.create!(processor: processor, event_type: type, event: event_payload(event))
|
|
32
|
+
Pay::Webhooks::ProcessJob.perform_later(record)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# "stripe", "paddle_billing", and so on, from the controller name
|
|
36
|
+
def processor
|
|
37
|
+
controller_name
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The request body as a plain hash, for processors that sign the request rather than hand us an event object
|
|
41
|
+
def verified_params
|
|
42
|
+
params.except(:action, :controller).permit!.as_json
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def invalid_signature
|
|
46
|
+
head :bad_request
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -1,28 +1,21 @@
|
|
|
1
1
|
module Pay
|
|
2
2
|
module Webhooks
|
|
3
|
-
class BraintreeController <
|
|
4
|
-
def create
|
|
5
|
-
queue_event(verified_event)
|
|
6
|
-
head :ok
|
|
7
|
-
rescue ::Braintree::InvalidSignature
|
|
8
|
-
head :bad_request
|
|
9
|
-
end
|
|
10
|
-
|
|
3
|
+
class BraintreeController < BaseController
|
|
11
4
|
private
|
|
12
5
|
|
|
13
|
-
def
|
|
14
|
-
|
|
6
|
+
def verified_event
|
|
7
|
+
Pay.braintree_gateway.webhook_notification.parse(params[:bt_signature], params[:bt_payload])
|
|
8
|
+
rescue ::Braintree::InvalidSignature => e
|
|
9
|
+
raise Pay::Braintree::Error, e
|
|
10
|
+
end
|
|
15
11
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
event_type: event.kind,
|
|
19
|
-
event: {bt_signature: params[:bt_signature], bt_payload: params[:bt_payload]}
|
|
20
|
-
)
|
|
21
|
-
Pay::Webhooks::ProcessJob.perform_later(record)
|
|
12
|
+
def event_type(event)
|
|
13
|
+
event.kind
|
|
22
14
|
end
|
|
23
15
|
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
# The parsed notification can't be serialized, so store the signed payload and parse it again in the job
|
|
17
|
+
def event_payload(event)
|
|
18
|
+
{bt_signature: params[:bt_signature], bt_payload: params[:bt_payload]}
|
|
26
19
|
end
|
|
27
20
|
end
|
|
28
21
|
end
|
|
@@ -1,41 +1,23 @@
|
|
|
1
1
|
module Pay
|
|
2
2
|
module Webhooks
|
|
3
|
-
class LemonSqueezyController <
|
|
4
|
-
def create
|
|
5
|
-
if valid_signature?(request.headers["X-Signature"])
|
|
6
|
-
queue_event(verify_params.as_json)
|
|
7
|
-
head :ok
|
|
8
|
-
else
|
|
9
|
-
head :bad_request
|
|
10
|
-
end
|
|
11
|
-
rescue Pay::LemonSqueezy::Error
|
|
12
|
-
head :bad_request
|
|
13
|
-
end
|
|
14
|
-
|
|
3
|
+
class LemonSqueezyController < BaseController
|
|
15
4
|
private
|
|
16
5
|
|
|
17
|
-
def
|
|
18
|
-
|
|
6
|
+
def verified_event
|
|
7
|
+
raise Pay::LemonSqueezy::Error, "Unable to verify Lemon Squeezy webhook signature" unless valid_signature?(request.headers["X-Signature"])
|
|
8
|
+
verified_params
|
|
9
|
+
end
|
|
19
10
|
|
|
20
|
-
|
|
21
|
-
|
|
11
|
+
def event_type(event)
|
|
12
|
+
event.dig("meta", "event_name")
|
|
22
13
|
end
|
|
23
14
|
|
|
24
|
-
# Pass Lemon Squeezy signature from request.headers["X-Signature"]
|
|
25
15
|
def valid_signature?(signature)
|
|
26
16
|
return false if signature.blank?
|
|
27
17
|
|
|
28
|
-
|
|
29
|
-
data = request.raw_post
|
|
30
|
-
digest = OpenSSL::Digest.new("sha256")
|
|
31
|
-
|
|
32
|
-
hmac = OpenSSL::HMAC.hexdigest(digest, key, data)
|
|
18
|
+
hmac = OpenSSL::HMAC.hexdigest("sha256", Pay::LemonSqueezy.signing_secret.to_s, request.raw_post)
|
|
33
19
|
ActiveSupport::SecurityUtils.secure_compare(hmac, signature)
|
|
34
20
|
end
|
|
35
|
-
|
|
36
|
-
def verify_params
|
|
37
|
-
params.except(:action, :controller).permit!
|
|
38
|
-
end
|
|
39
21
|
end
|
|
40
22
|
end
|
|
41
23
|
end
|
|
@@ -1,48 +1,26 @@
|
|
|
1
1
|
module Pay
|
|
2
2
|
module Webhooks
|
|
3
|
-
class PaddleBillingController <
|
|
4
|
-
def create
|
|
5
|
-
if valid_signature?(request.headers["Paddle-Signature"])
|
|
6
|
-
queue_event(verify_params.as_json)
|
|
7
|
-
head :ok
|
|
8
|
-
else
|
|
9
|
-
head :bad_request
|
|
10
|
-
end
|
|
11
|
-
rescue Pay::PaddleBilling::Error
|
|
12
|
-
head :bad_request
|
|
13
|
-
end
|
|
14
|
-
|
|
3
|
+
class PaddleBillingController < BaseController
|
|
15
4
|
private
|
|
16
5
|
|
|
17
|
-
def
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
record = Pay::Webhook.create!(processor: :paddle_billing, event_type: params[:event_type], event: event)
|
|
21
|
-
Pay::Webhooks::ProcessJob.perform_later(record)
|
|
6
|
+
def verified_event
|
|
7
|
+
raise Pay::PaddleBilling::Error, "Unable to verify Paddle webhook signature" unless valid_signature?(request.headers["Paddle-Signature"])
|
|
8
|
+
verified_params
|
|
22
9
|
end
|
|
23
10
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
ts_part, h1_part = paddle_signature.split(";")
|
|
29
|
-
_, ts = ts_part.split("=")
|
|
30
|
-
_, h1 = h1_part.split("=")
|
|
31
|
-
|
|
32
|
-
signed_payload = "#{ts}:#{request.raw_post}"
|
|
11
|
+
def event_type(event)
|
|
12
|
+
event["event_type"]
|
|
13
|
+
end
|
|
33
14
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
15
|
+
# The header looks like "ts=1671552777;h1=eb4d0dc8..."
|
|
16
|
+
def valid_signature?(signature)
|
|
17
|
+
parts = signature.to_s.split(";").filter_map { |part| part.split("=", 2) if part.include?("=") }.to_h
|
|
18
|
+
ts, h1 = parts.values_at("ts", "h1")
|
|
19
|
+
return false if ts.blank? || h1.blank?
|
|
37
20
|
|
|
38
|
-
hmac = OpenSSL::HMAC.hexdigest(
|
|
39
|
-
return false if h1.nil? || hmac.bytesize != h1.bytesize
|
|
21
|
+
hmac = OpenSSL::HMAC.hexdigest("sha256", Pay::PaddleBilling.signing_secret.to_s, "#{ts}:#{request.raw_post}")
|
|
40
22
|
ActiveSupport::SecurityUtils.secure_compare(hmac, h1)
|
|
41
23
|
end
|
|
42
|
-
|
|
43
|
-
def verify_params
|
|
44
|
-
params.except(:action, :controller).permit!
|
|
45
|
-
end
|
|
46
24
|
end
|
|
47
25
|
end
|
|
48
26
|
end
|
|
@@ -1,31 +1,16 @@
|
|
|
1
1
|
module Pay
|
|
2
2
|
module Webhooks
|
|
3
|
-
class PaddleClassicController <
|
|
4
|
-
def create
|
|
5
|
-
queue_event(verified_event)
|
|
6
|
-
head :ok
|
|
7
|
-
rescue Pay::PaddleClassic::Error
|
|
8
|
-
head :bad_request
|
|
9
|
-
end
|
|
10
|
-
|
|
3
|
+
class PaddleClassicController < BaseController
|
|
11
4
|
private
|
|
12
5
|
|
|
13
|
-
def queue_event(event)
|
|
14
|
-
return unless Pay::Webhooks.delegator.listening?("paddle_classic.#{params[:alert_name]}")
|
|
15
|
-
|
|
16
|
-
record = Pay::Webhook.create!(processor: :paddle_classic, event_type: params[:alert_name], event: event)
|
|
17
|
-
Pay::Webhooks::ProcessJob.perform_later(record)
|
|
18
|
-
end
|
|
19
|
-
|
|
20
6
|
def verified_event
|
|
21
|
-
event =
|
|
22
|
-
|
|
23
|
-
return event if verifier.verify
|
|
7
|
+
event = verified_params
|
|
8
|
+
return event if Pay::PaddleClassic::Webhooks::SignatureVerifier.new(event).verify
|
|
24
9
|
raise Pay::PaddleClassic::Error, "Unable to verify Paddle webhook event"
|
|
25
10
|
end
|
|
26
11
|
|
|
27
|
-
def
|
|
28
|
-
|
|
12
|
+
def event_type(event)
|
|
13
|
+
event["alert_name"]
|
|
29
14
|
end
|
|
30
15
|
end
|
|
31
16
|
end
|
|
@@ -1,46 +1,30 @@
|
|
|
1
1
|
module Pay
|
|
2
2
|
module Webhooks
|
|
3
|
-
class StripeController <
|
|
4
|
-
def create
|
|
5
|
-
event = verified_event
|
|
6
|
-
queue_event(event) if event.livemode || Pay::Stripe.webhook_receive_test_events
|
|
7
|
-
head :ok
|
|
8
|
-
rescue ::Stripe::SignatureVerificationError => e
|
|
9
|
-
log_error(e)
|
|
10
|
-
head :bad_request
|
|
11
|
-
end
|
|
12
|
-
|
|
3
|
+
class StripeController < BaseController
|
|
13
4
|
private
|
|
14
5
|
|
|
15
|
-
def queue_event(event)
|
|
16
|
-
return unless Pay::Webhooks.delegator.listening?("stripe.#{event.type}")
|
|
17
|
-
|
|
18
|
-
record = Pay::Webhook.create!(processor: :stripe, event_type: event.type, event: event)
|
|
19
|
-
Pay::Webhooks::ProcessJob.perform_later(record)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
6
|
def verified_event
|
|
23
7
|
payload = request.body.read
|
|
24
8
|
signature = request.headers["Stripe-Signature"]
|
|
25
|
-
|
|
9
|
+
secrets = Array.wrap(Pay::Stripe.signing_secret)
|
|
10
|
+
raise Pay::Stripe::Error, "Cannot verify signature without a Stripe signing secret" if secrets.empty?
|
|
26
11
|
|
|
27
|
-
|
|
12
|
+
# Several secrets can be configured while rotating; accept the first that verifies
|
|
13
|
+
secrets.each_with_index do |secret, i|
|
|
28
14
|
return ::Stripe::Webhook.construct_event(payload, signature, secret.to_s)
|
|
29
|
-
rescue ::Stripe::SignatureVerificationError
|
|
30
|
-
|
|
31
|
-
|
|
15
|
+
rescue ::Stripe::SignatureVerificationError => e
|
|
16
|
+
next unless i == secrets.length - 1
|
|
17
|
+
logger.error e.message
|
|
18
|
+
raise Pay::Stripe::Error, e
|
|
32
19
|
end
|
|
33
20
|
end
|
|
34
21
|
|
|
35
|
-
def
|
|
36
|
-
|
|
37
|
-
return Array.wrap(secret) if secret
|
|
38
|
-
raise ::Stripe::SignatureVerificationError.new("Cannot verify signature without a Stripe signing secret", signature, http_body: payload)
|
|
22
|
+
def event_type(event)
|
|
23
|
+
event.type
|
|
39
24
|
end
|
|
40
25
|
|
|
41
|
-
def
|
|
42
|
-
|
|
43
|
-
e.backtrace.each { |line| logger.error " #{line}" }
|
|
26
|
+
def queue?(event)
|
|
27
|
+
event.livemode || Pay::Stripe.webhook_receive_test_events
|
|
44
28
|
end
|
|
45
29
|
end
|
|
46
30
|
end
|
|
@@ -1,20 +1,14 @@
|
|
|
1
1
|
module Pay
|
|
2
2
|
module Braintree
|
|
3
3
|
class Charge < Pay::Charge
|
|
4
|
-
|
|
5
|
-
object ||= Pay.braintree_gateway.transaction.find(charge_id)
|
|
4
|
+
extend Pay::Sync
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
def self.sync(charge_id, object: nil)
|
|
7
|
+
sync_with_retries do
|
|
8
|
+
transaction = object || Pay.braintree_gateway.transaction.find(charge_id)
|
|
9
|
+
return unless (pay_customer = find_pay_customer(transaction.customer_details.id))
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
|
|
12
|
-
try += 1
|
|
13
|
-
if try <= retries
|
|
14
|
-
sleep 0.1
|
|
15
|
-
retry
|
|
16
|
-
else
|
|
17
|
-
raise
|
|
11
|
+
pay_customer.save_transaction(transaction)
|
|
18
12
|
end
|
|
19
13
|
end
|
|
20
14
|
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
module Pay
|
|
2
2
|
module Braintree
|
|
3
3
|
class PaymentMethod < Pay::PaymentMethod
|
|
4
|
-
|
|
5
|
-
object ||= Pay.braintree_gateway.payment_method.find(id)
|
|
4
|
+
extend Pay::Sync
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
def self.sync(id, object: nil)
|
|
7
|
+
sync_with_retries do
|
|
8
|
+
payment_method = object || Pay.braintree_gateway.payment_method.find(id)
|
|
9
|
+
return unless (pay_customer = find_pay_customer(payment_method.customer_id))
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
pay_customer.save_payment_method(payment_method, default: payment_method.default?)
|
|
12
|
+
end
|
|
11
13
|
end
|
|
12
14
|
|
|
13
15
|
# Sets payment method as default
|
|
@@ -1,49 +1,44 @@
|
|
|
1
1
|
module Pay
|
|
2
2
|
module Braintree
|
|
3
3
|
class Subscription < Pay::Subscription
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if object.status == "Canceled"
|
|
27
|
-
attributes[:ends_at] = object.updated_at
|
|
28
|
-
|
|
29
|
-
# Set grace period for subscriptions that are marked to be canceled
|
|
30
|
-
elsif object.status == "Active" && object.number_of_billing_cycles
|
|
31
|
-
attributes[:ends_at] = object.paid_through_date.end_of_day
|
|
32
|
-
end
|
|
4
|
+
extend Pay::Sync
|
|
5
|
+
|
|
6
|
+
def self.sync(subscription_id, object: nil, name: nil)
|
|
7
|
+
sync_with_retries do
|
|
8
|
+
subscription = object || Pay.braintree_gateway.subscription.find(subscription_id)
|
|
9
|
+
|
|
10
|
+
# The customer is only reachable through the subscription's payment method
|
|
11
|
+
payment_method = Pay.braintree_gateway.payment_method.find(subscription.payment_method_token)
|
|
12
|
+
return unless (pay_customer = find_pay_customer(payment_method.customer_id))
|
|
13
|
+
|
|
14
|
+
# Sync the PaymentMethod since we've got it
|
|
15
|
+
pay_customer.save_payment_method(payment_method, default: payment_method.default?)
|
|
16
|
+
|
|
17
|
+
attributes = {
|
|
18
|
+
created_at: subscription.created_at,
|
|
19
|
+
current_period_end: subscription.billing_period_end_date,
|
|
20
|
+
current_period_start: subscription.billing_period_start_date,
|
|
21
|
+
payment_method_id: subscription.payment_method_token,
|
|
22
|
+
processor_plan: subscription.plan_id,
|
|
23
|
+
status: subscription.status.parameterize(separator: "_"),
|
|
24
|
+
trial_ends_at: (subscription.created_at + subscription.trial_duration.send(subscription.trial_duration_unit) if subscription.trial_period)
|
|
25
|
+
}
|
|
33
26
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
27
|
+
# Canceled subscriptions should have access through the paid_through_date or updated_at
|
|
28
|
+
if subscription.status == "Canceled"
|
|
29
|
+
attributes[:ends_at] = subscription.updated_at
|
|
30
|
+
|
|
31
|
+
# Set grace period for subscriptions that are marked to be canceled
|
|
32
|
+
elsif subscription.status == "Active" && subscription.number_of_billing_cycles
|
|
33
|
+
attributes[:ends_at] = subscription.paid_through_date.end_of_day
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
if (pay_subscription = find_by(customer: pay_customer, processor_id: subscription.id))
|
|
37
|
+
pay_subscription.with_lock { pay_subscription.update!(attributes) }
|
|
38
|
+
else
|
|
39
|
+
name ||= Pay.default_product_name
|
|
40
|
+
create!(attributes.merge(customer: pay_customer, name: name, processor_id: subscription.id))
|
|
41
|
+
end
|
|
47
42
|
end
|
|
48
43
|
end
|
|
49
44
|
|
|
@@ -75,7 +70,7 @@ module Pay
|
|
|
75
70
|
end
|
|
76
71
|
|
|
77
72
|
def change_quantity(quantity, **options)
|
|
78
|
-
raise
|
|
73
|
+
raise Pay::NotSupportedError, "Braintree does not support setting quantity on subscriptions"
|
|
79
74
|
end
|
|
80
75
|
|
|
81
76
|
def paused?
|
|
@@ -83,7 +78,7 @@ module Pay
|
|
|
83
78
|
end
|
|
84
79
|
|
|
85
80
|
def pause
|
|
86
|
-
raise
|
|
81
|
+
raise Pay::NotSupportedError, "Braintree does not support pausing subscriptions"
|
|
87
82
|
end
|
|
88
83
|
|
|
89
84
|
def resumable?
|
data/app/models/pay/charge.rb
CHANGED
data/app/models/pay/customer.rb
CHANGED
|
@@ -8,10 +8,8 @@ module Pay
|
|
|
8
8
|
|
|
9
9
|
scope :active, -> { where(deleted_at: nil) }
|
|
10
10
|
scope :deleted, -> { where.not(deleted_at: nil) }
|
|
11
|
-
scope :not_fake_processor, -> { where.not(processor: :fake_processor) }
|
|
12
11
|
|
|
13
12
|
validates :processor, presence: true
|
|
14
|
-
validates :processor_id, allow_blank: true, uniqueness: {scope: :processor, case_sensitive: true}
|
|
15
13
|
|
|
16
14
|
# Account(s) for marketplace payments
|
|
17
15
|
store_accessor :data, :braintree_account
|
|
@@ -58,7 +56,7 @@ module Pay
|
|
|
58
56
|
end
|
|
59
57
|
|
|
60
58
|
def has_incomplete_payment?
|
|
61
|
-
subscriptions.
|
|
59
|
+
subscriptions.where(status: %w[incomplete past_due]).exists?
|
|
62
60
|
end
|
|
63
61
|
|
|
64
62
|
def customer_name
|
|
@@ -83,12 +81,6 @@ module Pay
|
|
|
83
81
|
# If these match, consider it a generic trial
|
|
84
82
|
subscription.trial_ends_at == subscription.ends_at
|
|
85
83
|
end
|
|
86
|
-
|
|
87
|
-
# Attempts to pay all past_due subscription invoices to bring them back to active state
|
|
88
|
-
# Pass in `statuses: []` if you would like to only include specific subscription statuses
|
|
89
|
-
def retry_past_due_subscriptions!(status: [:past_due])
|
|
90
|
-
subscriptions.where(status: Array.wrap(status)).each(&:pay_open_invoices)
|
|
91
|
-
end
|
|
92
84
|
end
|
|
93
85
|
end
|
|
94
86
|
|